├── Estrattore_Immagini.py ├── Frame_Extractor.py ├── LEGGIMI.md ├── LICENSE ├── README.md └── requirements.txt /Estrattore_Immagini.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import numpy as np 4 | import json 5 | import argparse 6 | import time 7 | from datetime import datetime, timedelta 8 | import sys 9 | import shutil 10 | from pathlib import Path 11 | from tqdm import tqdm 12 | import concurrent.futures 13 | import importlib.util 14 | import torch 15 | from scenedetect import open_video, SceneManager, FrameTimecode 16 | from scenedetect.detectors import ContentDetector, ThresholdDetector 17 | 18 | # Verifica se è disponibile PyTorch con supporto CUDA 19 | def check_gpu_support(): 20 | """ 21 | Verifica se è disponibile il supporto GPU usando PyTorch. 22 | Restituisce: (disponibilità_cuda, nome_gpu, info_cuda) 23 | """ 24 | try: 25 | cuda_available = torch.cuda.is_available() 26 | if cuda_available: 27 | gpu_name = torch.cuda.get_device_name(0) 28 | cuda_info = { 29 | 'name': gpu_name, 30 | 'total_memory': torch.cuda.get_device_properties(0).total_memory, 31 | 'cuda_version': torch.version.cuda 32 | } 33 | return True, gpu_name, cuda_info 34 | except Exception as e: 35 | print(f"Errore durante il controllo GPU con PyTorch: {str(e)}") 36 | return False, "GPU non disponibile", {} 37 | 38 | return False, "GPU non disponibile", {} 39 | 40 | # Verifica supporto GPU 41 | CUDA_AVAILABLE, GPU_NAME, CUDA_INFO = check_gpu_support() 42 | 43 | # Configurazione predefinita 44 | DEFAULT_CONFIG = { 45 | 'max_frames': 2000, 46 | 'sharpness_window': 7, 47 | 'use_gpu': CUDA_AVAILABLE, 48 | 'distribution_method': 'proportional', 49 | 'min_scene_duration': 10.0, # secondi 50 | 'max_frames_per_scene': 5, 51 | 'output_format': 'jpg', 52 | 'jpg_quality': 95, 53 | 'output_dir': 'frame_estratti', 54 | 'scene_threshold': 27.0, 55 | 'frames_per_10s': 1, # Quanti frame estrarre ogni 10 secondi di scena 56 | 'batch_size': 4 if CUDA_AVAILABLE else 1, # Elabora più frame contemporaneamente 57 | 'start_time': "00:00:00", # Punto di inizio analisi (HH:MM:SS) 58 | 'end_time': "", # Punto di fine analisi (vuoto = fino alla fine del video) 59 | 'use_time_range': False # Se utilizzare l'intervallo temporale specificato 60 | } 61 | 62 | def format_time(seconds): 63 | """Converte secondi in formato HH:MM:SS""" 64 | td = timedelta(seconds=seconds) 65 | return str(td).split('.')[0] 66 | 67 | def time_to_seconds(time_str): 68 | """Converte una stringa di tempo HH:MM:SS in secondi""" 69 | if not time_str: 70 | return 0 71 | 72 | parts = time_str.split(':') 73 | if len(parts) == 3: # HH:MM:SS 74 | h, m, s = parts 75 | return int(h) * 3600 + int(m) * 60 + float(s) 76 | elif len(parts) == 2: # MM:SS 77 | m, s = parts 78 | return int(m) * 60 + float(s) 79 | else: # Solo secondi 80 | return float(time_str) 81 | 82 | def seconds_to_time(seconds): 83 | """Converte secondi in formato HH:MM:SS""" 84 | h = int(seconds // 3600) 85 | m = int((seconds % 3600) // 60) 86 | s = seconds % 60 87 | return f"{h:02d}:{m:02d}:{s:06.3f}"[:8] 88 | 89 | # Function to install required packages if needed 90 | def ensure_gpu_packages(): 91 | """Verifica e suggerisce l'installazione dei pacchetti GPU se necessario""" 92 | if not CUDA_AVAILABLE: 93 | print("\n⚠️ Supporto GPU non rilevato!") 94 | print("Per abilitare l'accelerazione GPU:") 95 | print("1. Assicurati di avere installato i driver NVIDIA") 96 | print("2. Installa i pacchetti necessari con:") 97 | print(" pip install -r requirements_gpu.txt") 98 | print("\nNOTA: Lo script funzionerà comunque in modalità CPU") 99 | 100 | choice = input("\nVuoi continuare senza accelerazione GPU? (s/n): ").lower() 101 | if choice not in ['s', 'si', 'sì', 'y', 'yes']: 102 | print("\nInstallazione interrotta. Installa i requisiti GPU e riprova.") 103 | sys.exit(1) 104 | return CUDA_AVAILABLE 105 | 106 | def calculate_sharpness_cpu(image): 107 | """ 108 | Calcola il livello di nitidezza di un'immagine usando la varianza del Laplaciano (CPU). 109 | Valori più alti = immagine più nitida. 110 | """ 111 | # Converti in scala di grigi se necessario 112 | if len(image.shape) == 3: 113 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 114 | else: 115 | gray = image 116 | 117 | # Calcola il Laplaciano e la sua varianza 118 | lap_var = cv2.Laplacian(gray, cv2.CV_64F).var() 119 | return lap_var 120 | 121 | def calculate_sharpness_pytorch(image, device=None): 122 | """ 123 | Calcola il livello di nitidezza di un'immagine usando PyTorch. 124 | Valori più alti = immagine più nitida. 125 | """ 126 | if device is None: 127 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 128 | 129 | try: 130 | # Converti in scala di grigi se necessario 131 | if len(image.shape) == 3: 132 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 133 | else: 134 | gray = image 135 | 136 | # Converti in tensore PyTorch e sposta su GPU 137 | img_tensor = torch.from_numpy(gray.astype(np.float32)).to(device) 138 | 139 | # Crea kernel Laplaciano 140 | laplacian_kernel = torch.tensor([ 141 | [0, 1, 0], 142 | [1, -4, 1], 143 | [0, 1, 0] 144 | ], dtype=torch.float32, device=device).view(1, 1, 3, 3) 145 | 146 | # Aggiungi dimensioni batch e canale 147 | if len(img_tensor.shape) == 2: 148 | img_tensor = img_tensor.unsqueeze(0).unsqueeze(0) 149 | 150 | # Applica il filtro Laplaciano 151 | with torch.no_grad(): 152 | laplacian = torch.nn.functional.conv2d(img_tensor, laplacian_kernel, padding=1) 153 | 154 | # Calcola la varianza (indice di nitidezza) 155 | var = torch.var(laplacian).item() 156 | 157 | return var 158 | except Exception as e: 159 | print(f"⚠️ Errore GPU durante calcolo nitidezza: {str(e)}") 160 | print("⚠️ Cambio a modalità CPU...") 161 | # Fallback a calcolo CPU se ci sono errori 162 | return calculate_sharpness_cpu(image) 163 | 164 | def find_sharpest_frame_cpu(cap, target_frame, window_size=5): 165 | """ 166 | Cerca il frame più nitido in una finestra di frame intorno a quello target (CPU). 167 | Restituisce l'indice del frame più nitido e il frame stesso. 168 | """ 169 | start_frame = max(0, target_frame - window_size) 170 | end_frame = target_frame + window_size 171 | 172 | best_sharpness = -1 173 | best_frame = None 174 | best_idx = -1 175 | 176 | for idx in range(start_frame, end_frame + 1): 177 | cap.set(cv2.CAP_PROP_POS_FRAMES, idx) 178 | ret, frame = cap.read() 179 | 180 | if ret: 181 | sharpness = calculate_sharpness_cpu(frame) 182 | 183 | if sharpness > best_sharpness: 184 | best_sharpness = sharpness 185 | best_frame = frame 186 | best_idx = idx 187 | 188 | # Ritorna all'indice originale 189 | cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) 190 | 191 | return best_idx, best_frame, best_sharpness 192 | 193 | def find_sharpest_frame_pytorch(cap, target_frame, window_size=5, use_gpu=False): 194 | """ 195 | Cerca il frame più nitido in una finestra di frame intorno a quello target usando PyTorch. 196 | Restituisce l'indice del frame più nitido e il frame stesso. 197 | """ 198 | # Se GPU non è richiesta o non disponibile, usa CPU 199 | if not use_gpu or not torch.cuda.is_available(): 200 | return find_sharpest_frame_cpu(cap, target_frame, window_size) 201 | 202 | start_frame = max(0, target_frame - window_size) 203 | end_frame = target_frame + window_size 204 | 205 | best_sharpness = -1 206 | best_frame = None 207 | best_idx = -1 208 | 209 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 210 | 211 | try: 212 | for idx in range(start_frame, end_frame + 1): 213 | cap.set(cv2.CAP_PROP_POS_FRAMES, idx) 214 | ret, frame = cap.read() 215 | 216 | if ret: 217 | sharpness = calculate_sharpness_pytorch(frame, device) 218 | 219 | if sharpness > best_sharpness: 220 | best_sharpness = sharpness 221 | best_frame = frame 222 | best_idx = idx 223 | 224 | # Ritorna all'indice originale 225 | cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) 226 | 227 | return best_idx, best_frame, best_sharpness 228 | except Exception as e: 229 | print(f"⚠️ Errore durante l'elaborazione GPU: {str(e)}") 230 | print("⚠️ Passaggio a modalità CPU...") 231 | return find_sharpest_frame_cpu(cap, target_frame, window_size) 232 | 233 | def process_frame_batch_pytorch(cap, frame_indices, window_size=5): 234 | """ 235 | Processa un batch di frame in parallelo usando PyTorch. 236 | Restituisce una lista di tuple (frame_idx, best_idx, best_frame, sharpness) 237 | """ 238 | if not torch.cuda.is_available(): 239 | results = [] 240 | for idx in frame_indices: 241 | best_idx, best_frame, sharpness = find_sharpest_frame_cpu(cap, idx, window_size) 242 | results.append((idx, best_idx, best_frame, sharpness)) 243 | return results 244 | 245 | results = [] 246 | device = torch.device("cuda") 247 | 248 | for frame_idx in frame_indices: 249 | # Cerca nella finestra del frame 250 | best_idx, best_frame, best_sharpness = find_sharpest_frame_pytorch( 251 | cap, frame_idx, window_size, True 252 | ) 253 | results.append((frame_idx, best_idx, best_frame, best_sharpness)) 254 | 255 | return results 256 | 257 | def calculate_frames_for_scene(scene_duration, config): 258 | """ 259 | Calcola il numero di frame da estrarre per una scena basandosi sulla durata. 260 | """ 261 | if config['distribution_method'] == 'fixed': 262 | return min(config['max_frames_per_scene'], 1) 263 | else: # proportional 264 | # Calcola quanti frame in base alla durata (1 frame ogni X secondi) 265 | frames_to_extract = max(1, int(scene_duration / (10.0 / config['frames_per_10s']))) 266 | return min(frames_to_extract, config['max_frames_per_scene']) 267 | 268 | def extract_frames_from_scenes(video_path, config): 269 | """ 270 | Estrae frame da scene rilevate in un video, scegliendo quelli più nitidi 271 | """ 272 | # Ottieni nome del film dal percorso 273 | video_filename = os.path.basename(video_path) 274 | video_name = os.path.splitext(video_filename)[0] 275 | 276 | print(f"\n{'='*70}") 277 | print(f" ESTRAZIONE FRAME DA {video_name.upper()}") 278 | print(f"{'='*70}\n") 279 | 280 | # Crea la directory di output 281 | output_dir = os.path.expanduser(config['output_dir']) 282 | os.makedirs(output_dir, exist_ok=True) 283 | 284 | # Crea una sottocartella specifica per questo film 285 | film_output_dir = os.path.join(output_dir, video_name) 286 | os.makedirs(film_output_dir, exist_ok=True) 287 | 288 | # Apri il video 289 | video = open_video(video_path) 290 | 291 | # Ottieni informazioni video 292 | video_fps = video.frame_rate 293 | total_frames = video.duration.get_frames() 294 | video_duration = total_frames / video_fps 295 | 296 | # Converti i punti di inizio e fine in secondi e frame 297 | start_seconds = time_to_seconds(config['start_time']) if config['use_time_range'] else 0 298 | end_seconds = time_to_seconds(config['end_time']) if config['use_time_range'] and config['end_time'] else float('inf') 299 | 300 | # Converti in frame 301 | start_frame = int(start_seconds * video_fps) if config['use_time_range'] else 0 302 | end_frame = int(end_seconds * video_fps) if config['use_time_range'] and end_seconds < float('inf') else int(total_frames) 303 | 304 | # Se stiamo usando un intervallo di tempo, modifica il video passato al manager di scene 305 | if config['use_time_range']: 306 | # Crea frame timecodes per inizio e fine 307 | start_timecode = FrameTimecode(timecode=start_frame, fps=video_fps) 308 | end_timecode = FrameTimecode(timecode=min(end_frame, total_frames), fps=video_fps) 309 | else: 310 | start_timecode = None 311 | end_timecode = None 312 | 313 | print(f"📹 Informazioni video:") 314 | print(f" • File: {video_filename}") 315 | print(f" • FPS: {video_fps:.2f}") 316 | print(f" • Durata: {format_time(video_duration)}") 317 | print(f" • Frame totali: {total_frames}") 318 | 319 | # Mostra informazioni su intervalli temporali solo se abilitati 320 | if config['use_time_range']: 321 | if start_seconds > 0: 322 | print(f" • Punto di inizio: {config['start_time']} ({format_time(start_seconds)})") 323 | print(f" • Frame di inizio: {start_frame}") 324 | if end_seconds < float('inf'): 325 | print(f" • Punto di fine: {config['end_time']} ({format_time(end_seconds)})") 326 | print(f" • Frame di fine: {end_frame}") 327 | 328 | print(f" • Porzione da analizzare: {format_time(end_seconds - start_seconds)}") 329 | 330 | # Mostra stato GPU 331 | if config['use_gpu'] and CUDA_AVAILABLE: 332 | print(f" • Accelerazione GPU: ✅ Attiva") 333 | print(f" • GPU rilevata: {GPU_NAME}") 334 | if 'total_memory' in CUDA_INFO: 335 | print(f" • Memoria GPU: {CUDA_INFO['total_memory'] / (1024*1024):.1f} MB") 336 | print(f" • Batch size: {config['batch_size']}") 337 | else: 338 | print(f" • Accelerazione GPU: ❌ Disattiva") 339 | 340 | print(f" • Finestra ricerca nitidezza: ±{config['sharpness_window']} frame") 341 | print(f" • Metodo distribuzione: {config['distribution_method'].capitalize()}") 342 | print(f" • Frame per 10s di scena: {config['frames_per_10s']}") 343 | print() 344 | 345 | # Inizializza detector per trovare scene 346 | scene_manager = SceneManager() 347 | detector = ContentDetector(threshold=config['scene_threshold']) 348 | scene_manager.add_detector(detector) 349 | 350 | print("🎬 Inizio rilevamento scene...") 351 | 352 | # SOLUZIONE: Se stiamo usando un intervallo temporale, dobbiamo prima assicurarci di settare i frame di inizio/fine 353 | # per analizzare solo quella porzione del video 354 | 355 | if config['use_time_range']: 356 | print(f" • Analisi porzione video: {format_time(start_seconds)} - {format_time(min(end_seconds, video_duration))}") 357 | 358 | # Crea una sottosezione del video usando OpenCV per limitare l'area di analisi 359 | # Questa è una soluzione molto più efficiente e diretta 360 | 361 | # Apri il video con OpenCV 362 | cap = cv2.VideoCapture(video_path) 363 | if not cap.isOpened(): 364 | print("⚠️ Errore nell'apertura del video.") 365 | return 366 | 367 | # Imposta la posizione iniziale del video 368 | cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) 369 | 370 | # Crea un file video temporaneo per la porzione di interesse 371 | temp_path = os.path.join(os.path.dirname(video_path), f"temp_{int(time.time())}.mp4") 372 | 373 | # Ottiene i dettagli video necessari per il writer 374 | width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) 375 | height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) 376 | fps = cap.get(cv2.CAP_PROP_FPS) 377 | 378 | # Crea un writer video per il file temporaneo 379 | fourcc = cv2.VideoWriter_fourcc(*'mp4v') 380 | out = cv2.VideoWriter(temp_path, fourcc, fps, (width, height)) 381 | 382 | # Numero di frame da estrarre 383 | frames_to_extract = end_frame - start_frame 384 | 385 | print(f" • Preparazione file temporaneo per l'analisi della porzione...") 386 | 387 | # Leggi e scrivi i frame nella porzione di interesse 388 | frame_count = 0 389 | while cap.isOpened() and frame_count < frames_to_extract: 390 | ret, frame = cap.read() 391 | if not ret: 392 | break 393 | 394 | out.write(frame) 395 | frame_count += 1 396 | 397 | # Mostra progresso 398 | if frame_count % 100 == 0: 399 | progress = (frame_count / frames_to_extract) * 100 400 | print(f" • Preparazione: {progress:.1f}% completato", end="\r") 401 | 402 | # Rilascia le risorse 403 | cap.release() 404 | out.release() 405 | print(f" • Preparazione completata: {frame_count} frame estratti") 406 | 407 | # Ora utilizza il file temporaneo per la rilevazione delle scene 408 | temp_video = open_video(temp_path) 409 | 410 | # Rileva le scene nel file temporaneo (che contiene solo la porzione di interesse) 411 | scene_manager.detect_scenes(temp_video, show_progress=True) 412 | 413 | # Ottieni le scene rilevate 414 | scene_list_relative = scene_manager.get_scene_list() 415 | 416 | # Converti le scene in coordinate assolute (rispetto al video originale) 417 | scene_list = [] 418 | for scene in scene_list_relative: 419 | scene_start, scene_end = scene 420 | # Aggiungi l'offset di inizio 421 | absolute_start = FrameTimecode(timecode=scene_start.get_frames() + start_frame, fps=video_fps) 422 | absolute_end = FrameTimecode(timecode=scene_end.get_frames() + start_frame, fps=video_fps) 423 | scene_list.append((absolute_start, absolute_end)) 424 | 425 | # Elimina il file temporaneo 426 | try: 427 | os.remove(temp_path) 428 | print(f" • File temporaneo eliminato") 429 | except OSError as e: 430 | print(f" • Errore nell'eliminazione del file temporaneo: {e}") 431 | else: 432 | print(" • Analisi intero video") 433 | scene_manager.detect_scenes(video, show_progress=True) 434 | scene_list = scene_manager.get_scene_list() 435 | 436 | # Ottieni lista scene rilevate 437 | num_scenes = len(scene_list) 438 | 439 | print(f"\n✅ Scene rilevate: {num_scenes}") 440 | print(f"🎯 Obiettivo massimo: {config['max_frames']} frame totali\n") 441 | 442 | # Calcola numero totale di frame da estrarre basandosi sulla durata delle scene 443 | total_estimated_frames = 0 444 | scene_frame_counts = [] 445 | 446 | for scene in scene_list: 447 | scene_start_time, scene_end_time = scene 448 | scene_duration = (scene_end_time.get_frames() - scene_start_time.get_frames()) / video_fps 449 | frames_for_scene = calculate_frames_for_scene(scene_duration, config) 450 | scene_frame_counts.append(frames_for_scene) 451 | total_estimated_frames += frames_for_scene 452 | 453 | # Adatta il numero di frame se superiamo il limite 454 | if total_estimated_frames > config['max_frames']: 455 | reduction_factor = config['max_frames'] / total_estimated_frames 456 | scene_frame_counts = [max(1, int(count * reduction_factor)) for count in scene_frame_counts] 457 | total_estimated_frames = sum(scene_frame_counts) 458 | 459 | print(f"📊 Strategia di estrazione:") 460 | print(f" • Frame previsti: {total_estimated_frames}") 461 | if config['distribution_method'] == 'proportional': 462 | print(f" • Distribuzione: Proporzionale alla durata delle scene") 463 | print(f" • Frame per 10s di scena: {config['frames_per_10s']}") 464 | else: 465 | print(f" • Distribuzione: Fissa ({config['max_frames_per_scene']} per scena)") 466 | print() 467 | 468 | # Estrai frame 469 | frame_count = 0 470 | start_time = time.time() 471 | 472 | print(f"{'='*70}") 473 | print(f" INIZIO ESTRAZIONE FRAME") 474 | print(f"{'='*70}\n") 475 | 476 | # Apri il video per estrazione 477 | cap = cv2.VideoCapture(video_path) 478 | 479 | # Prepara una coda di frame da processare per elaborazione batch 480 | for scene_idx, (scene, frames_to_extract) in enumerate(zip(scene_list, scene_frame_counts)): 481 | scene_start_time, scene_end_time = scene 482 | scene_start_frame = scene_start_time.get_frames() 483 | scene_end_frame = scene_end_time.get_frames() 484 | scene_duration = (scene_end_frame - scene_start_frame) / video_fps 485 | 486 | print(f"\n🎞️ Scena {scene_idx+1}/{num_scenes}") 487 | print(f" • Inizio: {format_time(scene_start_time.get_seconds())}") 488 | print(f" • Fine: {format_time(scene_end_time.get_seconds())}") 489 | print(f" • Durata: {scene_duration:.2f}s") 490 | print(f" • Frame da estrarre: {frames_to_extract}") 491 | 492 | # Se non ci sono frame da estrarre, passa alla prossima scena 493 | if frames_to_extract <= 0: 494 | continue 495 | 496 | # Calcola indici dei frame da estrarre 497 | if scene_end_frame - scene_start_frame <= frames_to_extract: 498 | # Prendi tutti i frame disponibili se ce ne sono meno del necessario 499 | frame_indices = list(range(int(scene_start_frame), int(scene_end_frame))) 500 | else: 501 | # Distribuisci uniformemente i frame nella scena 502 | step = (scene_end_frame - scene_start_frame) / frames_to_extract 503 | frame_indices = [int(scene_start_frame + i * step) for i in range(frames_to_extract)] 504 | 505 | # Se abbiamo raggiunto il limite massimo di frame, interrompi 506 | if frame_count >= config['max_frames']: 507 | print(f"\n🎯 Raggiunto limite massimo di {config['max_frames']} frame!") 508 | break 509 | 510 | # Estrazione batch con GPU 511 | if config['use_gpu'] and CUDA_AVAILABLE: 512 | # Suddividi i frame in batch 513 | batch_size = min(config['batch_size'], len(frame_indices)) 514 | batches = [frame_indices[i:i + batch_size] for i in range(0, len(frame_indices), batch_size)] 515 | 516 | print(f" • Elaborazione in {len(batches)} batch con GPU...") 517 | 518 | for batch_idx, batch in enumerate(batches): 519 | if frame_count >= config['max_frames']: 520 | break 521 | 522 | # Processa batch di frame 523 | results = process_frame_batch_pytorch(cap, batch, config['sharpness_window']) 524 | 525 | # Salva i risultati 526 | for i, (original_idx, best_idx, best_frame, sharpness) in enumerate(results): 527 | if frame_count >= config['max_frames']: 528 | break 529 | 530 | if best_frame is not None: 531 | # Usa il tempo del frame realmente scelto 532 | frame_time = best_idx / video_fps 533 | filename = f"scena_{scene_idx+1:03d}_{frame_time:.2f}s.{config['output_format']}" 534 | filepath = os.path.join(film_output_dir, filename) 535 | 536 | # Salva il frame 537 | if config['output_format'].lower() == 'jpg': 538 | cv2.imwrite(filepath, best_frame, [cv2.IMWRITE_JPEG_QUALITY, config['jpg_quality']]) 539 | else: 540 | cv2.imwrite(filepath, best_frame) 541 | 542 | frame_count += 1 543 | 544 | # Calcola il delta rispetto al frame inizialmente scelto 545 | frame_delta = best_idx - original_idx 546 | delta_info = f"(delta: {frame_delta:+d})" if frame_delta != 0 else "(frame originale)" 547 | 548 | # Calcola ETA 549 | if frame_count > 0: 550 | elapsed_time = time.time() - start_time 551 | frames_per_second = frame_count / elapsed_time 552 | frames_left = min(config['max_frames'], total_estimated_frames) - frame_count 553 | eta = frames_left / frames_per_second if frames_per_second > 0 else 0 554 | 555 | print(f" ✓ Frame {i+1}/{len(batch)} del batch {batch_idx+1}: {filename} | Nitidezza: {sharpness:.2f} {delta_info} | ETA: {format_time(eta)}") 556 | else: 557 | print(f" ✓ Frame {i+1}/{len(batch)} del batch {batch_idx+1}: {filename} | Nitidezza: {sharpness:.2f} {delta_info}") 558 | 559 | # Mostra progresso dopo ogni batch 560 | if frame_count > 0: 561 | progress_percentage = frame_count / total_estimated_frames * 100 562 | print(f" 📊 Progresso: {progress_percentage:.1f}% completato | {frame_count}/{total_estimated_frames} frame") 563 | else: 564 | # Estrazione standard (non parallela) 565 | for i, frame_idx in enumerate(frame_indices): 566 | if frame_count >= config['max_frames']: 567 | break 568 | 569 | # Trova il frame più nitido nella finestra 570 | best_idx, best_frame, sharpness = find_sharpest_frame_cpu( 571 | cap, frame_idx, config['sharpness_window'] 572 | ) 573 | 574 | if best_frame is not None: 575 | # Usa il tempo del frame realmente scelto 576 | frame_time = best_idx / video_fps 577 | filename = f"scena_{scene_idx+1:03d}_{frame_time:.2f}s.{config['output_format']}" 578 | filepath = os.path.join(film_output_dir, filename) 579 | 580 | # Salva il frame 581 | if config['output_format'].lower() == 'jpg': 582 | cv2.imwrite(filepath, best_frame, [cv2.IMWRITE_JPEG_QUALITY, config['jpg_quality']]) 583 | else: 584 | cv2.imwrite(filepath, best_frame) 585 | 586 | frame_count += 1 587 | 588 | # Calcola il delta rispetto al frame inizialmente scelto 589 | frame_delta = best_idx - frame_idx 590 | delta_info = f"(delta: {frame_delta:+d})" if frame_delta != 0 else "(frame originale)" 591 | 592 | # Calcola tempo rimanente 593 | elapsed_time = time.time() - start_time 594 | if frame_count > 0: 595 | avg_time_per_frame = elapsed_time / frame_count 596 | frames_left = min(config['max_frames'], total_estimated_frames) - frame_count 597 | eta = avg_time_per_frame * frames_left 598 | print(f" ✓ Frame {i+1}/{len(frame_indices)}: {filename} | Nitidezza: {sharpness:.2f} {delta_info} | ETA: {format_time(eta)}") 599 | else: 600 | print(f" ✓ Frame {i+1}/{len(frame_indices)}: {filename} | Nitidezza: {sharpness:.2f} {delta_info}") 601 | 602 | # Aggiorna statistiche dopo ogni scena 603 | progress_percentage = (scene_idx + 1) / num_scenes * 100 604 | scenes_left = num_scenes - scene_idx - 1 605 | if scenes_left > 0 and frame_count > 0: 606 | elapsed_time = time.time() - start_time 607 | frames_per_second = frame_count / elapsed_time 608 | estimated_total_time = total_estimated_frames / frames_per_second 609 | time_left = max(0, estimated_total_time - elapsed_time) 610 | 611 | print(f" 📊 Progresso: {progress_percentage:.1f}% completato | {frame_count}/{total_estimated_frames} frame | Tempo stimato: {format_time(time_left)}") 612 | 613 | # Chiudi il video 614 | cap.release() 615 | 616 | print(f"\n{'='*70}") 617 | print(f" ESTRAZIONE COMPLETATA!") 618 | print(f"{'='*70}\n") 619 | print(f"✨ Frame totali estratti: {frame_count}") 620 | print(f"⏱️ Tempo totale: {format_time(time.time() - start_time)}") 621 | print(f"📁 Output directory: {film_output_dir}") 622 | print(f"\nI frame estratti sono pronti per addestrare il tuo modello LoRA!") 623 | 624 | def find_video_files(directory): 625 | """Trova tutti i file video nella directory specificata""" 626 | video_extensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'] 627 | video_files = [] 628 | 629 | for file in os.listdir(directory): 630 | if any(file.lower().endswith(ext) for ext in video_extensions): 631 | video_files.append(file) 632 | 633 | return video_files 634 | 635 | def save_config(config, filepath): 636 | """Salva la configurazione in un file JSON""" 637 | with open(filepath, 'w') as f: 638 | json.dump(config, f, indent=4) 639 | 640 | def load_config(filepath): 641 | """Carica la configurazione da un file JSON""" 642 | if os.path.exists(filepath): 643 | with open(filepath, 'r') as f: 644 | loaded_config = json.load(f) 645 | 646 | # Assicurati che tutte le chiavi del DEFAULT_CONFIG siano presenti 647 | for key in DEFAULT_CONFIG: 648 | if key not in loaded_config: 649 | loaded_config[key] = DEFAULT_CONFIG[key] 650 | 651 | return loaded_config 652 | return DEFAULT_CONFIG.copy() 653 | 654 | def clear_screen(): 655 | """Pulisce lo schermo del terminale""" 656 | os.system('cls' if os.name == 'nt' else 'clear') 657 | 658 | def print_header(): 659 | """Stampa l'intestazione del programma""" 660 | print(f"\n{'='*70}") 661 | print(f" ESTRATTORE IMMAGINI") 662 | if CUDA_AVAILABLE: 663 | print(f" GPU: {GPU_NAME}") 664 | else: 665 | print(f" GPU: Non disponibile") 666 | print(f"{'='*70}\n") 667 | 668 | def print_menu(header, options, back_option=True): 669 | """Stampa un menu con opzioni numerate""" 670 | clear_screen() 671 | print_header() 672 | 673 | print(f"{header}\n") 674 | 675 | for i, option in enumerate(options): 676 | print(f"{i+1}. {option}") 677 | 678 | if back_option: 679 | print(f"\n0. Torna indietro") 680 | 681 | return input("\nScelta: ") 682 | 683 | def menu_intervallo_temporale(video_path, config): 684 | """Menu per impostare l'intervallo temporale di analisi""" 685 | video_filename = os.path.basename(video_path) 686 | 687 | # Ottieni durata del video 688 | cap = cv2.VideoCapture(video_path) 689 | if not cap.isOpened(): 690 | print("\n⚠️ Impossibile aprire il video per verificare la durata.") 691 | return config 692 | 693 | # Ottieni informazioni video 694 | fps = cap.get(cv2.CAP_PROP_FPS) 695 | total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 696 | duration = total_frames / fps 697 | duration_str = format_time(duration) 698 | cap.release() 699 | 700 | clear_screen() 701 | print_header() 702 | print(f"INTERVALLO TEMPORALE - {video_filename}\n") 703 | print(f"Durata totale del video: {duration_str}\n") 704 | # Visualizza stato attuale 705 | if config['use_time_range']: 706 | start_time = config['start_time'] 707 | end_time = config['end_time'] if config['end_time'] else "Fine del video" 708 | print(f"Intervallo attuale: {start_time} → {end_time}\n") 709 | else: 710 | print("Intervallo temporale: Non utilizzato (intero video)\n") 711 | 712 | # Opzioni menu 713 | options = [ 714 | "Attiva/Disattiva intervallo temporale" + (" [Attivo]" if config['use_time_range'] else " [Disattivo]"), 715 | "Imposta punto di inizio", 716 | "Imposta punto di fine", 717 | "Usa intero video (reimposta intervallo)" 718 | ] 719 | 720 | choice = print_menu("Opzioni intervallo temporale:", options) 721 | 722 | if choice == '0': 723 | return config 724 | elif choice == '1': 725 | # Attiva/Disattiva intervallo 726 | config['use_time_range'] = not config['use_time_range'] 727 | status = "attivato" if config['use_time_range'] else "disattivato" 728 | print(f"\n✅ Intervallo temporale {status}.") 729 | input("\nPremi INVIO per continuare...") 730 | elif choice == '2' and config['use_time_range']: 731 | # Imposta punto di inizio 732 | clear_screen() 733 | print_header() 734 | print(f"IMPOSTA PUNTO DI INIZIO - {video_filename}\n") 735 | print(f"Durata totale del video: {duration_str}") 736 | print(f"Formato: HH:MM:SS o MM:SS\n") 737 | 738 | new_value = input("Nuovo punto di inizio (lascia vuoto per inizio video): ") 739 | 740 | if new_value: 741 | try: 742 | seconds = time_to_seconds(new_value) 743 | # Verifica se l'orario è valido 744 | if seconds >= 0: 745 | if seconds < duration: 746 | config['start_time'] = new_value 747 | print(f"\n✅ Punto di inizio impostato: {new_value} ({format_time(seconds)})") 748 | 749 | # Aggiorna anche il punto di fine se necessario 750 | end_seconds = time_to_seconds(config['end_time']) if config['end_time'] else float('inf') 751 | if end_seconds < seconds: 752 | config['end_time'] = "" 753 | print(f"⚠️ Punto di fine reimpostato alla fine del video (era prima del punto di inizio)") 754 | else: 755 | print(f"\n⚠️ Il punto di inizio è oltre la durata del video ({duration_str}).") 756 | else: 757 | print("\n⚠️ Il tempo deve essere positivo.") 758 | except ValueError: 759 | print("\n⚠️ Formato tempo non valido. Usa HH:MM:SS o MM:SS.") 760 | else: 761 | config['start_time'] = "00:00:00" 762 | print("\n✅ Punto di inizio reimpostato all'inizio del video.") 763 | 764 | input("\nPremi INVIO per continuare...") 765 | elif choice == '3' and config['use_time_range']: 766 | # Imposta punto di fine 767 | clear_screen() 768 | print_header() 769 | print(f"IMPOSTA PUNTO DI FINE - {video_filename}\n") 770 | print(f"Durata totale del video: {duration_str}") 771 | print(f"Punto di inizio attuale: {config['start_time']}") 772 | print(f"Formato: HH:MM:SS o MM:SS\n") 773 | 774 | new_value = input("Nuovo punto di fine (lascia vuoto per fine video): ") 775 | 776 | if new_value: 777 | try: 778 | seconds = time_to_seconds(new_value) 779 | # Verifica se l'orario è valido 780 | if seconds >= 0: 781 | # Verifica se il punto di fine è dopo il punto di inizio 782 | start_seconds = time_to_seconds(config['start_time']) 783 | if seconds > start_seconds: 784 | config['end_time'] = new_value 785 | print(f"\n✅ Punto di fine impostato: {new_value} ({format_time(seconds)})") 786 | if seconds > duration: 787 | print(f"⚠️ Nota: Il punto di fine è oltre la durata del video ({duration_str}), verrà usata la fine del video.") 788 | else: 789 | print(f"\n⚠️ Il punto di fine deve essere successivo al punto di inizio ({config['start_time']}).") 790 | else: 791 | print("\n⚠️ Il tempo deve essere positivo.") 792 | except ValueError: 793 | print("\n⚠️ Formato tempo non valido. Usa HH:MM:SS o MM:SS.") 794 | else: 795 | config['end_time'] = "" 796 | print("\n✅ Punto di fine reimpostato alla fine del video.") 797 | 798 | input("\nPremi INVIO per continuare...") 799 | elif choice == '4': 800 | # Reimposta intervallo (usa intero video) 801 | config['use_time_range'] = False 802 | config['start_time'] = "00:00:00" 803 | config['end_time'] = "" 804 | print("\n✅ Intervallo temporale reimpostato (intero video).") 805 | input("\nPremi INVIO per continuare...") 806 | else: 807 | print("\n⚠️ Scelta non valida. Riprova.") 808 | input("\nPremi INVIO per continuare...") 809 | 810 | # Ritorna al menu intervallo 811 | return menu_intervallo_temporale(video_path, config) 812 | 813 | def menu_principale(): 814 | """Menu principale dell'applicazione""" 815 | # Verifica GPU all'avvio 816 | ensure_gpu_packages() 817 | 818 | config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json') 819 | config = load_config(config_file) 820 | 821 | while True: 822 | clear_screen() 823 | print_header() 824 | 825 | # Trovare tutti i video 826 | current_dir = os.path.dirname(os.path.abspath(__file__)) 827 | video_files = find_video_files(current_dir) 828 | 829 | if not video_files: 830 | print("❌ Nessun file video trovato nella directory corrente.") 831 | print("Per favore, aggiungi file video (mp4, mkv, avi, mov, wmv) nella stessa cartella dello script.") 832 | input("\nPremi INVIO per uscire...") 833 | sys.exit(1) 834 | 835 | print("Film trovati nella cartella:") 836 | for i, file in enumerate(video_files): 837 | print(f"{i+1}. {file}") 838 | 839 | choice = input("\nSeleziona un film (1-{}) o 'q' per uscire: ".format(len(video_files))) 840 | 841 | if choice.lower() == 'q': 842 | print("\nArrivederci!") 843 | sys.exit(0) 844 | 845 | try: 846 | idx = int(choice) - 1 847 | if 0 <= idx < len(video_files): 848 | selected_video = video_files[idx] 849 | video_path = os.path.join(current_dir, selected_video) 850 | 851 | # Menu per il video selezionato 852 | menu_video(video_path, config, config_file) 853 | else: 854 | print("\n⚠️ Scelta non valida. Riprova.") 855 | input("\nPremi INVIO per continuare...") 856 | except ValueError: 857 | print("\n⚠️ Inserisci un numero valido.") 858 | input("\nPremi INVIO per continuare...") 859 | 860 | def menu_video(video_path, config, config_file): 861 | """Menu per il video selezionato""" 862 | video_filename = os.path.basename(video_path) 863 | 864 | while True: 865 | options = [ 866 | f"Avvia estrazione con parametri predefiniti", 867 | f"Personalizza parametri", 868 | f"Imposta intervallo temporale", 869 | f"Visualizza descrizione dei parametri", 870 | f"Ripristina parametri predefiniti" 871 | ] 872 | 873 | choice = print_menu(f"Video selezionato: {video_filename}", options) 874 | 875 | if choice == '0': 876 | return 877 | elif choice == '1': 878 | # Avvia con parametri predefiniti 879 | extract_frames_from_scenes(video_path, config) 880 | input("\nPremi INVIO per tornare al menu...") 881 | elif choice == '2': 882 | # Personalizza parametri 883 | config = menu_personalizza_parametri(video_path, config, config_file) 884 | elif choice == '3': 885 | # Imposta intervallo temporale 886 | config = menu_intervallo_temporale(video_path, config) 887 | save_config(config, config_file) 888 | elif choice == '4': 889 | # Visualizza descrizione parametri 890 | menu_descrizione_parametri() 891 | elif choice == '5': 892 | # Ripristina parametri predefiniti 893 | config = DEFAULT_CONFIG.copy() 894 | save_config(config, config_file) 895 | print("\n✅ Parametri ripristinati ai valori predefiniti.") 896 | input("\nPremi INVIO per continuare...") 897 | else: 898 | print("\n⚠️ Scelta non valida. Riprova.") 899 | input("\nPremi INVIO per continuare...") 900 | 901 | def menu_personalizza_parametri(video_path, config, config_file): 902 | """Menu per personalizzare i parametri""" 903 | while True: 904 | options = [ 905 | f"Numero massimo di frame [{config['max_frames']}]", 906 | f"Finestra ricerca nitidezza [{config['sharpness_window']}]", 907 | f"Utilizzo GPU [{'Attivo' if config['use_gpu'] else 'Disattivo'}]", 908 | f"Distribuzione frame [{'Proporzionale' if config['distribution_method'] == 'proportional' else 'Fissa'}]", 909 | f"Frame ogni 10 secondi [{config['frames_per_10s']}]", 910 | f"Max frame per scena [{config['max_frames_per_scene']}]", 911 | f"Formato output [{config['output_format']}]", 912 | f"Qualità JPG [{config['jpg_quality']}]", 913 | f"Directory output [{config['output_dir']}]", 914 | f"Soglia rilevamento scene [{config['scene_threshold']}]", 915 | f"Dimensione batch GPU [{config['batch_size']}]", 916 | f"Avvia con questi parametri" 917 | ] 918 | 919 | choice = print_menu("PERSONALIZZA PARAMETRI", options) 920 | 921 | if choice == '0': 922 | # Salva configurazione e torna indietro 923 | save_config(config, config_file) 924 | return config 925 | elif choice == '1': 926 | # Numero massimo di frame 927 | try: 928 | new_value = int(input("\nInserisci nuovo valore per numero massimo di frame: ")) 929 | if new_value > 0: 930 | config['max_frames'] = new_value 931 | print(f"\n✅ Valore aggiornato: {new_value}") 932 | else: 933 | print("\n⚠️ Il valore deve essere maggiore di 0.") 934 | except ValueError: 935 | print("\n⚠️ Inserisci un numero valido.") 936 | elif choice == '2': 937 | # Finestra ricerca nitidezza 938 | try: 939 | new_value = int(input("\nInserisci nuovo valore per finestra ricerca nitidezza: ")) 940 | if new_value > 0: 941 | config['sharpness_window'] = new_value 942 | print(f"\n✅ Valore aggiornato: {new_value}") 943 | else: 944 | print("\n⚠️ Il valore deve essere maggiore di 0.") 945 | except ValueError: 946 | print("\n⚠️ Inserisci un numero valido.") 947 | elif choice == '3': 948 | # Utilizzo GPU 949 | if not CUDA_AVAILABLE: 950 | print("\n⚠️ GPU non disponibile sul tuo sistema.") 951 | input("\nPremi INVIO per continuare...") 952 | continue 953 | 954 | gpu_choice = input("\nAttivare accelerazione GPU? (s/n): ").lower() 955 | if gpu_choice in ['s', 'si', 'sì', 'y', 'yes']: 956 | config['use_gpu'] = True 957 | print("\n✅ Accelerazione GPU attivata.") 958 | elif gpu_choice in ['n', 'no']: 959 | config['use_gpu'] = False 960 | print("\n✅ Accelerazione GPU disattivata.") 961 | else: 962 | print("\n⚠️ Scelta non valida.") 963 | elif choice == '4': 964 | # Metodo distribuzione frame 965 | dist_choice = input("\nScegli metodo distribuzione (p=proporzionale, f=fisso): ").lower() 966 | if dist_choice in ['p', 'proporzionale']: 967 | config['distribution_method'] = 'proportional' 968 | print("\n✅ Distribuzione proporzionale impostata.") 969 | elif dist_choice in ['f', 'fisso']: 970 | config['distribution_method'] = 'fixed' 971 | print("\n✅ Distribuzione fissa impostata.") 972 | else: 973 | print("\n⚠️ Scelta non valida.") 974 | elif choice == '5': 975 | # Frame ogni 10 secondi 976 | try: 977 | new_value = float(input("\nInserisci numero di frame da estrarre ogni 10 secondi: ")) 978 | if new_value > 0: 979 | config['frames_per_10s'] = new_value 980 | print(f"\n✅ Valore aggiornato: {new_value}") 981 | else: 982 | print("\n⚠️ Il valore deve essere maggiore di 0.") 983 | except ValueError: 984 | print("\n⚠️ Inserisci un numero valido.") 985 | elif choice == '6': 986 | # Max frame per scena 987 | try: 988 | new_value = int(input("\nInserisci massimo numero di frame per scena: ")) 989 | if new_value > 0: 990 | config['max_frames_per_scene'] = new_value 991 | print(f"\n✅ Valore aggiornato: {new_value}") 992 | else: 993 | print("\n⚠️ Il valore deve essere maggiore di 0.") 994 | except ValueError: 995 | print("\n⚠️ Inserisci un numero valido.") 996 | elif choice == '7': 997 | # Formato output 998 | format_choice = input("\nScegli formato output (jpg/png): ").lower() 999 | if format_choice in ['jpg', 'jpeg']: 1000 | config['output_format'] = 'jpg' 1001 | print("\n✅ Formato JPG impostato.") 1002 | elif format_choice in ['png']: 1003 | config['output_format'] = 'png' 1004 | print("\n✅ Formato PNG impostato.") 1005 | else: 1006 | print("\n⚠️ Formato non valido. Utilizza jpg o png.") 1007 | elif choice == '8': 1008 | # Qualità JPG 1009 | try: 1010 | new_value = int(input("\nInserisci qualità JPG (1-100): ")) 1011 | if 1 <= new_value <= 100: 1012 | config['jpg_quality'] = new_value 1013 | print(f"\n✅ Qualità impostata: {new_value}") 1014 | else: 1015 | print("\n⚠️ La qualità deve essere tra 1 e 100.") 1016 | except ValueError: 1017 | print("\n⚠️ Inserisci un numero valido.") 1018 | elif choice == '9': 1019 | # Directory output 1020 | new_value = input("\nInserisci directory di output: ") 1021 | if new_value: 1022 | config['output_dir'] = new_value 1023 | print(f"\n✅ Directory impostata: {new_value}") 1024 | else: 1025 | print("\n⚠️ Directory non valida.") 1026 | elif choice == '10': 1027 | # Soglia rilevamento scene 1028 | try: 1029 | new_value = float(input("\nInserisci soglia rilevamento scene (1-100, valori più bassi=più scene): ")) 1030 | if 1 <= new_value <= 100: 1031 | config['scene_threshold'] = new_value 1032 | print(f"\n✅ Soglia impostata: {new_value}") 1033 | else: 1034 | print("\n⚠️ La soglia deve essere tra 1 e 100.") 1035 | except ValueError: 1036 | print("\n⚠️ Inserisci un numero valido.") 1037 | elif choice == '11': 1038 | # Dimensione batch GPU 1039 | if not CUDA_AVAILABLE: 1040 | print("\n⚠️ GPU non disponibile. Impostazione ignorata.") 1041 | input("\nPremi INVIO per continuare...") 1042 | continue 1043 | 1044 | try: 1045 | new_value = int(input("\nInserisci dimensione batch GPU (1-10): ")) 1046 | if 1 <= new_value <= 10: 1047 | config['batch_size'] = new_value 1048 | print(f"\n✅ Dimensione batch impostata: {new_value}") 1049 | else: 1050 | print("\n⚠️ La dimensione deve essere tra 1 e 10.") 1051 | except ValueError: 1052 | print("\n⚠️ Inserisci un numero valido.") 1053 | elif choice == '12': 1054 | # Avvia con questi parametri 1055 | save_config(config, config_file) 1056 | extract_frames_from_scenes(video_path, config) 1057 | input("\nPremi INVIO per tornare al menu...") 1058 | else: 1059 | print("\n⚠️ Scelta non valida. Riprova.") 1060 | input("\nPremi INVIO per continuare...") 1061 | 1062 | # Salva configurazione 1063 | save_config(config, config_file) 1064 | 1065 | return config 1066 | 1067 | def menu_descrizione_parametri(): 1068 | """Mostra descrizione dettagliata dei parametri""" 1069 | clear_screen() 1070 | print_header() 1071 | 1072 | print("DESCRIZIONE DEI PARAMETRI\n") 1073 | 1074 | print("1. Numero massimo di frame") 1075 | print(" Determina quanti frame estrarre in totale dal video.") 1076 | print(" Valori più alti = dataset più grande, ma più tempo di elaborazione.") 1077 | print(" Consigliato: 2000-5000 per la maggior parte dei film.") 1078 | 1079 | print("\n2. Finestra ricerca nitidezza") 1080 | print(" Numero di frame prima e dopo da analizzare per trovare il frame più nitido.") 1081 | print(" Valori più alti = frame più nitidi, ma più tempo di elaborazione.") 1082 | print(" Consigliato: 5-10 per bilanciare qualità e velocità.") 1083 | 1084 | print("\n3. Utilizzo GPU") 1085 | print(" Attiva/disattiva l'accelerazione GPU per le operazioni di elaborazione immagini.") 1086 | print(" Consigliato: Attivo se disponibile una GPU NVIDIA compatibile.") 1087 | 1088 | print("\n4. Distribuzione frame") 1089 | print(" 'Proporzionale': estrae più frame per scene lunghe, meno per scene brevi.") 1090 | print(" 'Fissa': estrae lo stesso numero di frame per ogni scena.") 1091 | print(" Consigliato: Proporzionale per una copertura migliore.") 1092 | 1093 | print("\n5. Frame ogni 10 secondi") 1094 | print(" Quanti frame estrarre ogni 10 secondi di scena (in modalità proporzionale).") 1095 | print(" Valori più alti = più frame per scene lunghe.") 1096 | print(" Consigliato: 1-2 per la maggior parte dei casi.") 1097 | 1098 | print("\n6. Max frame per scena") 1099 | print(" Limite superiore di frame estraibili da una singola scena.") 1100 | print(" Consigliato: 5-10 per evitare troppe immagini simili.") 1101 | 1102 | print("\n7. Formato output") 1103 | print(" 'jpg': più compatto, leggermente inferiore in qualità.") 1104 | print(" 'png': qualità migliore, file più grandi.") 1105 | print(" Consigliato: jpg per la maggior parte dei casi.") 1106 | 1107 | print("\n8. Qualità JPG") 1108 | print(" Livello di qualità/compressione per i file JPG (1-100).") 1109 | print(" Valori più alti = qualità migliore, file più grandi.") 1110 | print(" Consigliato: 85-95 per un buon equilibrio.") 1111 | 1112 | print("\n9. Directory output") 1113 | print(" La cartella dove salvare i frame estratti.") 1114 | print(" Una sottocartella con il nome del film viene creata automaticamente.") 1115 | 1116 | print("\n10. Soglia rilevamento scene") 1117 | print(" Sensibilità nel rilevare cambi di scena (1-100).") 1118 | print(" Valori più bassi = più scene rilevate.") 1119 | print(" Consigliato: 25-35 per film standard, 15-25 per film con molti tagli rapidi.") 1120 | 1121 | print("\n11. Dimensione batch GPU") 1122 | print(" Numero di frame da elaborare contemporaneamente con la GPU.") 1123 | print(" Valori più alti = maggiore velocità, ma più memoria GPU richiesta.") 1124 | print(" Consigliato: 4-8 per GPU con 8GB+ di memoria, 2-4 per GPU con meno memoria.") 1125 | 1126 | print("\n12. Intervallo temporale") 1127 | print(" Definisce una porzione specifica del video da analizzare.") 1128 | print(" Utile per saltare titoli di testa/coda o concentrarsi su specifiche scene.") 1129 | print(" Si imposta tramite il menu 'Imposta intervallo temporale'.") 1130 | 1131 | input("\nPremi INVIO per tornare al menu precedente...") 1132 | 1133 | if __name__ == "__main__": 1134 | try: 1135 | menu_principale() 1136 | except KeyboardInterrupt: 1137 | print("\n\nEsecuzione interrotta dall'utente.") 1138 | except Exception as e: 1139 | print(f"\n\nErrore imprevisto: {str(e)}") 1140 | input("\nPremi INVIO per uscire...") 1141 | -------------------------------------------------------------------------------- /Frame_Extractor.py: -------------------------------------------------------------------------------- 1 | import os 2 | import cv2 3 | import numpy as np 4 | import json 5 | import argparse 6 | import time 7 | from datetime import datetime, timedelta 8 | import sys 9 | import shutil 10 | from pathlib import Path 11 | from tqdm import tqdm 12 | import concurrent.futures 13 | import importlib.util 14 | import torch 15 | from scenedetect import open_video, SceneManager, FrameTimecode 16 | from scenedetect.detectors import ContentDetector, ThresholdDetector 17 | 18 | # Check if PyTorch with CUDA support is available 19 | def check_gpu_support(): 20 | """ 21 | Check if GPU support is available using PyTorch. 22 | Returns: (cuda_available, gpu_name, cuda_info) 23 | """ 24 | try: 25 | cuda_available = torch.cuda.is_available() 26 | if cuda_available: 27 | gpu_name = torch.cuda.get_device_name(0) 28 | cuda_info = { 29 | 'name': gpu_name, 30 | 'total_memory': torch.cuda.get_device_properties(0).total_memory, 31 | 'cuda_version': torch.version.cuda 32 | } 33 | return True, gpu_name, cuda_info 34 | except Exception as e: 35 | print(f"Error during GPU check with PyTorch: {str(e)}") 36 | return False, "GPU not available", {} 37 | 38 | return False, "GPU not available", {} 39 | 40 | # Check GPU support 41 | CUDA_AVAILABLE, GPU_NAME, CUDA_INFO = check_gpu_support() 42 | 43 | # Default configuration 44 | DEFAULT_CONFIG = { 45 | 'max_frames': 2000, 46 | 'sharpness_window': 7, 47 | 'use_gpu': CUDA_AVAILABLE, 48 | 'distribution_method': 'proportional', 49 | 'min_scene_duration': 10.0, # seconds 50 | 'max_frames_per_scene': 5, 51 | 'output_format': 'jpg', 52 | 'jpg_quality': 95, 53 | 'output_dir': 'extracted_frames', 54 | 'scene_threshold': 27.0, 55 | 'frames_per_10s': 1, # How many frames to extract every 10 seconds of scene 56 | 'batch_size': 4 if CUDA_AVAILABLE else 1, # Process multiple frames simultaneously 57 | 'start_time': "00:00:00", # Analysis start point (HH:MM:SS) 58 | 'end_time': "", # Analysis end point (empty = until end of video) 59 | 'use_time_range': False # Whether to use the specified time range 60 | } 61 | 62 | def format_time(seconds): 63 | """Converts seconds to HH:MM:SS format""" 64 | td = timedelta(seconds=seconds) 65 | return str(td).split('.')[0] 66 | 67 | def time_to_seconds(time_str): 68 | """Converts a time string HH:MM:SS to seconds""" 69 | if not time_str: 70 | return 0 71 | 72 | parts = time_str.split(':') 73 | if len(parts) == 3: # HH:MM:SS 74 | h, m, s = parts 75 | return int(h) * 3600 + int(m) * 60 + float(s) 76 | elif len(parts) == 2: # MM:SS 77 | m, s = parts 78 | return int(m) * 60 + float(s) 79 | else: # Only seconds 80 | return float(time_str) 81 | 82 | def seconds_to_time(seconds): 83 | """Converts seconds to HH:MM:SS format""" 84 | h = int(seconds // 3600) 85 | m = int((seconds % 3600) // 60) 86 | s = seconds % 60 87 | return f"{h:02d}:{m:02d}:{s:06.3f}"[:8] 88 | 89 | # Function to install required packages if needed 90 | def ensure_gpu_packages(): 91 | """Checks and suggests installation of GPU packages if necessary""" 92 | if not CUDA_AVAILABLE: 93 | print("\n⚠️ GPU support not detected!") 94 | print("To enable GPU acceleration:") 95 | print("1. Make sure you have NVIDIA drivers installed") 96 | print("2. Install the necessary packages with:") 97 | print(" pip install -r requirements_gpu.txt") 98 | print("\nNOTE: The script will still run in CPU mode") 99 | 100 | choice = input("\nDo you want to continue without GPU acceleration? (y/n): ").lower() 101 | if choice not in ['y', 'yes']: 102 | print("\nInstallation aborted. Install GPU requirements and try again.") 103 | sys.exit(1) 104 | return CUDA_AVAILABLE 105 | 106 | def calculate_sharpness_cpu(image): 107 | """ 108 | Calculate the sharpness level of an image using Laplacian variance (CPU). 109 | Higher values = sharper image. 110 | """ 111 | # Convert to grayscale if necessary 112 | if len(image.shape) == 3: 113 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 114 | else: 115 | gray = image 116 | 117 | # Calculate Laplacian and its variance 118 | lap_var = cv2.Laplacian(gray, cv2.CV_64F).var() 119 | return lap_var 120 | 121 | def calculate_sharpness_pytorch(image, device=None): 122 | """ 123 | Calculate the sharpness level of an image using PyTorch. 124 | Higher values = sharper image. 125 | """ 126 | if device is None: 127 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 128 | 129 | try: 130 | # Convert to grayscale if necessary 131 | if len(image.shape) == 3: 132 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 133 | else: 134 | gray = image 135 | 136 | # Convert to PyTorch tensor and move to GPU 137 | img_tensor = torch.from_numpy(gray.astype(np.float32)).to(device) 138 | 139 | # Create Laplacian kernel 140 | laplacian_kernel = torch.tensor([ 141 | [0, 1, 0], 142 | [1, -4, 1], 143 | [0, 1, 0] 144 | ], dtype=torch.float32, device=device).view(1, 1, 3, 3) 145 | 146 | # Add batch and channel dimensions 147 | if len(img_tensor.shape) == 2: 148 | img_tensor = img_tensor.unsqueeze(0).unsqueeze(0) 149 | 150 | # Apply Laplacian filter 151 | with torch.no_grad(): 152 | laplacian = torch.nn.functional.conv2d(img_tensor, laplacian_kernel, padding=1) 153 | 154 | # Calculate variance (sharpness index) 155 | var = torch.var(laplacian).item() 156 | 157 | return var 158 | except Exception as e: 159 | print(f"⚠️ GPU error during sharpness calculation: {str(e)}") 160 | print("⚠️ Switching to CPU mode...") 161 | # Fallback to CPU calculation if there are errors 162 | return calculate_sharpness_cpu(image) 163 | 164 | def find_sharpest_frame_cpu(cap, target_frame, window_size=5): 165 | """ 166 | Search for the sharpest frame in a window around the target one (CPU). 167 | Returns the index of the sharpest frame and the frame itself. 168 | """ 169 | start_frame = max(0, target_frame - window_size) 170 | end_frame = target_frame + window_size 171 | 172 | best_sharpness = -1 173 | best_frame = None 174 | best_idx = -1 175 | 176 | for idx in range(start_frame, end_frame + 1): 177 | cap.set(cv2.CAP_PROP_POS_FRAMES, idx) 178 | ret, frame = cap.read() 179 | 180 | if ret: 181 | sharpness = calculate_sharpness_cpu(frame) 182 | 183 | if sharpness > best_sharpness: 184 | best_sharpness = sharpness 185 | best_frame = frame 186 | best_idx = idx 187 | 188 | # Return to original index 189 | cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) 190 | 191 | return best_idx, best_frame, best_sharpness 192 | 193 | def find_sharpest_frame_pytorch(cap, target_frame, window_size=5, use_gpu=False): 194 | """ 195 | Search for the sharpest frame in a window around the target one using PyTorch. 196 | Returns the index of the sharpest frame and the frame itself. 197 | """ 198 | # If GPU is not requested or not available, use CPU 199 | if not use_gpu or not torch.cuda.is_available(): 200 | return find_sharpest_frame_cpu(cap, target_frame, window_size) 201 | 202 | start_frame = max(0, target_frame - window_size) 203 | end_frame = target_frame + window_size 204 | 205 | best_sharpness = -1 206 | best_frame = None 207 | best_idx = -1 208 | 209 | device = torch.device("cuda" if torch.cuda.is_available() else "cpu") 210 | 211 | try: 212 | for idx in range(start_frame, end_frame + 1): 213 | cap.set(cv2.CAP_PROP_POS_FRAMES, idx) 214 | ret, frame = cap.read() 215 | 216 | if ret: 217 | sharpness = calculate_sharpness_pytorch(frame, device) 218 | 219 | if sharpness > best_sharpness: 220 | best_sharpness = sharpness 221 | best_frame = frame 222 | best_idx = idx 223 | 224 | # Return to original index 225 | cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame) 226 | 227 | return best_idx, best_frame, best_sharpness 228 | except Exception as e: 229 | print(f"⚠️ Error during GPU processing: {str(e)}") 230 | print("⚠️ Switching to CPU mode...") 231 | return find_sharpest_frame_cpu(cap, target_frame, window_size) 232 | 233 | def process_frame_batch_pytorch(cap, frame_indices, window_size=5): 234 | """ 235 | Process a batch of frames in parallel using PyTorch. 236 | Returns a list of tuples (frame_idx, best_idx, best_frame, sharpness) 237 | """ 238 | if not torch.cuda.is_available(): 239 | results = [] 240 | for idx in frame_indices: 241 | best_idx, best_frame, sharpness = find_sharpest_frame_cpu(cap, idx, window_size) 242 | results.append((idx, best_idx, best_frame, sharpness)) 243 | return results 244 | 245 | results = [] 246 | device = torch.device("cuda") 247 | 248 | for frame_idx in frame_indices: 249 | # Search in the frame window 250 | best_idx, best_frame, best_sharpness = find_sharpest_frame_pytorch( 251 | cap, frame_idx, window_size, True 252 | ) 253 | results.append((frame_idx, best_idx, best_frame, best_sharpness)) 254 | 255 | return results 256 | 257 | def calculate_frames_for_scene(scene_duration, config): 258 | """ 259 | Calculate the number of frames to extract for a scene based on its duration. 260 | """ 261 | if config['distribution_method'] == 'fixed': 262 | return min(config['max_frames_per_scene'], 1) 263 | else: # proportional 264 | # Calculate how many frames based on duration (1 frame every X seconds) 265 | frames_to_extract = max(1, int(scene_duration / (10.0 / config['frames_per_10s']))) 266 | return min(frames_to_extract, config['max_frames_per_scene']) 267 | 268 | def extract_frames_from_scenes(video_path, config): 269 | """ 270 | Extract frames from detected scenes in a video, choosing the sharpest ones 271 | """ 272 | # Get movie name from path 273 | video_filename = os.path.basename(video_path) 274 | video_name = os.path.splitext(video_filename)[0] 275 | 276 | print(f"\n{'='*70}") 277 | print(f" EXTRACTING FRAMES FROM {video_name.upper()}") 278 | print(f"{'='*70}\n") 279 | 280 | # Create output directory 281 | output_dir = os.path.expanduser(config['output_dir']) 282 | os.makedirs(output_dir, exist_ok=True) 283 | 284 | # Create a specific subfolder for this movie 285 | film_output_dir = os.path.join(output_dir, video_name) 286 | os.makedirs(film_output_dir, exist_ok=True) 287 | 288 | # Open the video 289 | video = open_video(video_path) 290 | 291 | # Get video information 292 | video_fps = video.frame_rate 293 | total_frames = video.duration.get_frames() 294 | video_duration = total_frames / video_fps 295 | 296 | # Convert start and end points to seconds and frames 297 | start_seconds = time_to_seconds(config['start_time']) if config['use_time_range'] else 0 298 | end_seconds = time_to_seconds(config['end_time']) if config['use_time_range'] and config['end_time'] else float('inf') 299 | 300 | # Convert to frames 301 | start_frame = int(start_seconds * video_fps) if config['use_time_range'] else 0 302 | end_frame = int(end_seconds * video_fps) if config['use_time_range'] and end_seconds < float('inf') else int(total_frames) 303 | 304 | # If we're using a time range, modify the video passed to the scene manager 305 | if config['use_time_range']: 306 | # Create frame timecodes for start and end 307 | start_timecode = FrameTimecode(timecode=start_frame, fps=video_fps) 308 | end_timecode = FrameTimecode(timecode=min(end_frame, total_frames), fps=video_fps) 309 | else: 310 | start_timecode = None 311 | end_timecode = None 312 | 313 | print(f"📹 Video information:") 314 | print(f" • File: {video_filename}") 315 | print(f" • FPS: {video_fps:.2f}") 316 | print(f" • Duration: {format_time(video_duration)}") 317 | print(f" • Total frames: {total_frames}") 318 | 319 | # Show time interval information only if enabled 320 | if config['use_time_range']: 321 | if start_seconds > 0: 322 | print(f" • Start point: {config['start_time']} ({format_time(start_seconds)})") 323 | print(f" • Start frame: {start_frame}") 324 | if end_seconds < float('inf'): 325 | print(f" • End point: {config['end_time']} ({format_time(end_seconds)})") 326 | print(f" • End frame: {end_frame}") 327 | 328 | print(f" • Portion to analyze: {format_time(end_seconds - start_seconds)}") 329 | 330 | # Show GPU status 331 | if config['use_gpu'] and CUDA_AVAILABLE: 332 | print(f" • GPU acceleration: ✅ Active") 333 | print(f" • Detected GPU: {GPU_NAME}") 334 | if 'total_memory' in CUDA_INFO: 335 | print(f" • GPU Memory: {CUDA_INFO['total_memory'] / (1024*1024):.1f} MB") 336 | print(f" • Batch size: {config['batch_size']}") 337 | else: 338 | print(f" • GPU acceleration: ❌ Inactive") 339 | 340 | print(f" • Sharpness search window: ±{config['sharpness_window']} frames") 341 | print(f" • Distribution method: {config['distribution_method'].capitalize()}") 342 | print(f" • Frames per 10s of scene: {config['frames_per_10s']}") 343 | print() 344 | 345 | # Initialize detector to find scenes 346 | scene_manager = SceneManager() 347 | detector = ContentDetector(threshold=config['scene_threshold']) 348 | scene_manager.add_detector(detector) 349 | 350 | print("🎬 Starting scene detection...") 351 | 352 | # SOLUTION: If we're using a time range, we need to first ensure we set the start/end frames 353 | # to analyze only that portion of the video 354 | 355 | if config['use_time_range']: 356 | print(f" • Analyzing video portion: {format_time(start_seconds)} - {format_time(min(end_seconds, video_duration))}") 357 | 358 | # Create a subsection of the video using OpenCV to limit the analysis area 359 | # This is a much more efficient and direct solution 360 | 361 | # Open the video with OpenCV 362 | cap = cv2.VideoCapture(video_path) 363 | if not cap.isOpened(): 364 | print("⚠️ Error opening the video.") 365 | return 366 | 367 | # Set the initial position of the video 368 | cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) 369 | 370 | # Create a temporary video file for the portion of interest 371 | temp_path = os.path.join(os.path.dirname(video_path), f"temp_{int(time.time())}.mp4") 372 | 373 | # Get video details needed for the writer 374 | width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) 375 | height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) 376 | fps = cap.get(cv2.CAP_PROP_FPS) 377 | 378 | # Create a video writer for the temporary file 379 | fourcc = cv2.VideoWriter_fourcc(*'mp4v') 380 | out = cv2.VideoWriter(temp_path, fourcc, fps, (width, height)) 381 | 382 | # Number of frames to extract 383 | frames_to_extract = end_frame - start_frame 384 | 385 | print(f" • Preparing temporary file for portion analysis...") 386 | 387 | # Read and write the frames in the portion of interest 388 | frame_count = 0 389 | while cap.isOpened() and frame_count < frames_to_extract: 390 | ret, frame = cap.read() 391 | if not ret: 392 | break 393 | 394 | out.write(frame) 395 | frame_count += 1 396 | 397 | # Show progress 398 | if frame_count % 100 == 0: 399 | progress = (frame_count / frames_to_extract) * 100 400 | print(f" • Preparation: {progress:.1f}% completed", end="\r") 401 | 402 | # Release resources 403 | cap.release() 404 | out.release() 405 | print(f" • Preparation completed: {frame_count} frames extracted") 406 | 407 | # Now use the temporary file for scene detection 408 | temp_video = open_video(temp_path) 409 | 410 | # Detect scenes in the temporary file (which contains only the portion of interest) 411 | scene_manager.detect_scenes(temp_video, show_progress=True) 412 | 413 | # Get the detected scenes 414 | scene_list_relative = scene_manager.get_scene_list() 415 | 416 | # Convert scenes to absolute coordinates (relative to the original video) 417 | scene_list = [] 418 | for scene in scene_list_relative: 419 | scene_start, scene_end = scene 420 | # Add the start offset 421 | absolute_start = FrameTimecode(timecode=scene_start.get_frames() + start_frame, fps=video_fps) 422 | absolute_end = FrameTimecode(timecode=scene_end.get_frames() + start_frame, fps=video_fps) 423 | scene_list.append((absolute_start, absolute_end)) 424 | 425 | # Delete the temporary file 426 | try: 427 | os.remove(temp_path) 428 | print(f" • Temporary file deleted") 429 | except OSError as e: 430 | print(f" • Error deleting temporary file: {e}") 431 | else: 432 | print(" • Analyzing entire video") 433 | scene_manager.detect_scenes(video, show_progress=True) 434 | scene_list = scene_manager.get_scene_list() 435 | 436 | # Get list of detected scenes 437 | num_scenes = len(scene_list) 438 | 439 | print(f"\n✅ Detected scenes: {num_scenes}") 440 | print(f"🎯 Maximum target: {config['max_frames']} total frames\n") 441 | 442 | # Calculate total number of frames to extract based on scene duration 443 | total_estimated_frames = 0 444 | scene_frame_counts = [] 445 | 446 | for scene in scene_list: 447 | scene_start_time, scene_end_time = scene 448 | scene_duration = (scene_end_time.get_frames() - scene_start_time.get_frames()) / video_fps 449 | frames_for_scene = calculate_frames_for_scene(scene_duration, config) 450 | scene_frame_counts.append(frames_for_scene) 451 | total_estimated_frames += frames_for_scene 452 | 453 | # Adjust the number of frames if we exceed the limit 454 | if total_estimated_frames > config['max_frames']: 455 | reduction_factor = config['max_frames'] / total_estimated_frames 456 | scene_frame_counts = [max(1, int(count * reduction_factor)) for count in scene_frame_counts] 457 | total_estimated_frames = sum(scene_frame_counts) 458 | 459 | print(f"📊 Extraction strategy:") 460 | print(f" • Expected frames: {total_estimated_frames}") 461 | if config['distribution_method'] == 'proportional': 462 | print(f" • Distribution: Proportional to scene duration") 463 | print(f" • Frames per 10s of scene: {config['frames_per_10s']}") 464 | else: 465 | print(f" • Distribution: Fixed ({config['max_frames_per_scene']} per scene)") 466 | print() 467 | 468 | # Extract frames 469 | frame_count = 0 470 | start_time = time.time() 471 | 472 | print(f"{'='*70}") 473 | print(f" STARTING FRAME EXTRACTION") 474 | print(f"{'='*70}\n") 475 | 476 | # Open the video for extraction 477 | cap = cv2.VideoCapture(video_path) 478 | 479 | # Prepare a queue of frames to process for batch processing 480 | for scene_idx, (scene, frames_to_extract) in enumerate(zip(scene_list, scene_frame_counts)): 481 | scene_start_time, scene_end_time = scene 482 | scene_start_frame = scene_start_time.get_frames() 483 | scene_end_frame = scene_end_time.get_frames() 484 | scene_duration = (scene_end_frame - scene_start_frame) / video_fps 485 | 486 | print(f"\n🎞️ Scene {scene_idx+1}/{num_scenes}") 487 | print(f" • Start: {format_time(scene_start_time.get_seconds())}") 488 | print(f" • End: {format_time(scene_end_time.get_seconds())}") 489 | print(f" • Duration: {scene_duration:.2f}s") 490 | print(f" • Frames to extract: {frames_to_extract}") 491 | 492 | # If there are no frames to extract, move to the next scene 493 | if frames_to_extract <= 0: 494 | continue 495 | 496 | # Calculate indices of frames to extract 497 | if scene_end_frame - scene_start_frame <= frames_to_extract: 498 | # Take all available frames if there are fewer than needed 499 | frame_indices = list(range(int(scene_start_frame), int(scene_end_frame))) 500 | else: 501 | # Distribute frames evenly in the scene 502 | step = (scene_end_frame - scene_start_frame) / frames_to_extract 503 | frame_indices = [int(scene_start_frame + i * step) for i in range(frames_to_extract)] 504 | 505 | # If we've reached the maximum frame limit, stop 506 | if frame_count >= config['max_frames']: 507 | print(f"\n🎯 Reached maximum limit of {config['max_frames']} frames!") 508 | break 509 | 510 | # Batch extraction with GPU 511 | if config['use_gpu'] and CUDA_AVAILABLE: 512 | # Split frames into batches 513 | batch_size = min(config['batch_size'], len(frame_indices)) 514 | batches = [frame_indices[i:i + batch_size] for i in range(0, len(frame_indices), batch_size)] 515 | 516 | print(f" • Processing in {len(batches)} batches with GPU...") 517 | 518 | for batch_idx, batch in enumerate(batches): 519 | if frame_count >= config['max_frames']: 520 | break 521 | 522 | # Process batch of frames 523 | results = process_frame_batch_pytorch(cap, batch, config['sharpness_window']) 524 | 525 | # Save the results 526 | for i, (original_idx, best_idx, best_frame, sharpness) in enumerate(results): 527 | if frame_count >= config['max_frames']: 528 | break 529 | 530 | if best_frame is not None: 531 | # Use the time of the frame actually chosen 532 | frame_time = best_idx / video_fps 533 | filename = f"scene_{scene_idx+1:03d}_{frame_time:.2f}s.{config['output_format']}" 534 | filepath = os.path.join(film_output_dir, filename) 535 | 536 | # Save the frame 537 | if config['output_format'].lower() == 'jpg': 538 | cv2.imwrite(filepath, best_frame, [cv2.IMWRITE_JPEG_QUALITY, config['jpg_quality']]) 539 | else: 540 | cv2.imwrite(filepath, best_frame) 541 | 542 | frame_count += 1 543 | 544 | # Calculate delta relative to initially chosen frame 545 | frame_delta = best_idx - original_idx 546 | delta_info = f"(delta: {frame_delta:+d})" if frame_delta != 0 else "(original frame)" 547 | 548 | # Calculate ETA 549 | if frame_count > 0: 550 | elapsed_time = time.time() - start_time 551 | frames_per_second = frame_count / elapsed_time 552 | frames_left = min(config['max_frames'], total_estimated_frames) - frame_count 553 | eta = frames_left / frames_per_second if frames_per_second > 0 else 0 554 | 555 | print(f" ✓ Frame {i+1}/{len(batch)} of batch {batch_idx+1}: {filename} | Sharpness: {sharpness:.2f} {delta_info} | ETA: {format_time(eta)}") 556 | else: 557 | print(f" ✓ Frame {i+1}/{len(batch)} of batch {batch_idx+1}: {filename} | Sharpness: {sharpness:.2f} {delta_info}") 558 | 559 | # Show progress after each batch 560 | if frame_count > 0: 561 | progress_percentage = frame_count / total_estimated_frames * 100 562 | print(f" 📊 Progress: {progress_percentage:.1f}% completed | {frame_count}/{total_estimated_frames} frames") 563 | else: 564 | # Standard extraction (non-parallel) 565 | for i, frame_idx in enumerate(frame_indices): 566 | if frame_count >= config['max_frames']: 567 | break 568 | 569 | # Find the sharpest frame in the window 570 | best_idx, best_frame, sharpness = find_sharpest_frame_cpu( 571 | cap, frame_idx, config['sharpness_window'] 572 | ) 573 | 574 | if best_frame is not None: 575 | # Use the time of the frame actually chosen 576 | frame_time = best_idx / video_fps 577 | filename = f"scene_{scene_idx+1:03d}_{frame_time:.2f}s.{config['output_format']}" 578 | filepath = os.path.join(film_output_dir, filename) 579 | 580 | # Save the frame 581 | if config['output_format'].lower() == 'jpg': 582 | cv2.imwrite(filepath, best_frame, [cv2.IMWRITE_JPEG_QUALITY, config['jpg_quality']]) 583 | else: 584 | cv2.imwrite(filepath, best_frame) 585 | 586 | frame_count += 1 587 | 588 | # Calculate delta relative to initially chosen frame 589 | frame_delta = best_idx - frame_idx 590 | delta_info = f"(delta: {frame_delta:+d})" if frame_delta != 0 else "(original frame)" 591 | 592 | # Calculate remaining time 593 | elapsed_time = time.time() - start_time 594 | if frame_count > 0: 595 | avg_time_per_frame = elapsed_time / frame_count 596 | frames_left = min(config['max_frames'], total_estimated_frames) - frame_count 597 | eta = avg_time_per_frame * frames_left 598 | print(f" ✓ Frame {i+1}/{len(frame_indices)}: {filename} | Sharpness: {sharpness:.2f} {delta_info} | ETA: {format_time(eta)}") 599 | else: 600 | print(f" ✓ Frame {i+1}/{len(frame_indices)}: {filename} | Sharpness: {sharpness:.2f} {delta_info}") 601 | 602 | # Update statistics after each scene 603 | progress_percentage = (scene_idx + 1) / num_scenes * 100 604 | scenes_left = num_scenes - scene_idx - 1 605 | if scenes_left > 0 and frame_count > 0: 606 | elapsed_time = time.time() - start_time 607 | frames_per_second = frame_count / elapsed_time 608 | estimated_total_time = total_estimated_frames / frames_per_second 609 | time_left = max(0, estimated_total_time - elapsed_time) 610 | 611 | print(f" 📊 Progress: {progress_percentage:.1f}% completed | {frame_count}/{total_estimated_frames} frames | Estimated time: {format_time(time_left)}") 612 | 613 | # Close the video 614 | cap.release() 615 | 616 | print(f"\n{'='*70}") 617 | print(f" EXTRACTION COMPLETED!") 618 | print(f"{'='*70}\n") 619 | print(f"✨ Total frames extracted: {frame_count}") 620 | print(f"⏱️ Total time: {format_time(time.time() - start_time)}") 621 | print(f"📁 Output directory: {film_output_dir}") 622 | print(f"\nThe extracted frames are ready for training your LoRA model!") 623 | 624 | def find_video_files(directory): 625 | """Find all video files in the specified directory""" 626 | video_extensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'] 627 | video_files = [] 628 | 629 | for file in os.listdir(directory): 630 | if any(file.lower().endswith(ext) for ext in video_extensions): 631 | video_files.append(file) 632 | 633 | return video_files 634 | 635 | def save_config(config, filepath): 636 | """Save the configuration to a JSON file""" 637 | with open(filepath, 'w') as f: 638 | json.dump(config, f, indent=4) 639 | 640 | def load_config(filepath): 641 | """Load the configuration from a JSON file""" 642 | if os.path.exists(filepath): 643 | with open(filepath, 'r') as f: 644 | loaded_config = json.load(f) 645 | 646 | # Make sure all keys from DEFAULT_CONFIG are present 647 | for key in DEFAULT_CONFIG: 648 | if key not in loaded_config: 649 | loaded_config[key] = DEFAULT_CONFIG[key] 650 | 651 | return loaded_config 652 | return DEFAULT_CONFIG.copy() 653 | 654 | def clear_screen(): 655 | """Clear the terminal screen""" 656 | os.system('cls' if os.name == 'nt' else 'clear') 657 | 658 | def print_header(): 659 | """Print the program header""" 660 | print(f"\n{'='*70}") 661 | print(f" FRAME EXTRACTOR") 662 | if CUDA_AVAILABLE: 663 | print(f" GPU: {GPU_NAME}") 664 | else: 665 | print(f" GPU: Not available") 666 | print(f"{'='*70}\n") 667 | 668 | def print_menu(header, options, back_option=True): 669 | """Print a menu with numbered options""" 670 | clear_screen() 671 | print_header() 672 | 673 | print(f"{header}\n") 674 | 675 | for i, option in enumerate(options): 676 | print(f"{i+1}. {option}") 677 | 678 | if back_option: 679 | print(f"\n0. Go back") 680 | 681 | return input("\nChoice: ") 682 | 683 | def menu_time_range(video_path, config): 684 | """Menu for setting the time range for analysis""" 685 | video_filename = os.path.basename(video_path) 686 | 687 | # Get video duration 688 | cap = cv2.VideoCapture(video_path) 689 | if not cap.isOpened(): 690 | print("\n⚠️ Unable to open the video to check duration.") 691 | return config 692 | 693 | # Get video information 694 | fps = cap.get(cv2.CAP_PROP_FPS) 695 | total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) 696 | duration = total_frames / fps 697 | duration_str = format_time(duration) 698 | cap.release() 699 | 700 | clear_screen() 701 | print_header() 702 | print(f"TIME RANGE - {video_filename}\n") 703 | print(f"Total video duration: {duration_str}\n") 704 | # Display current status 705 | if config['use_time_range']: 706 | start_time = config['start_time'] 707 | end_time = config['end_time'] if config['end_time'] else "End of video" 708 | print(f"Current range: {start_time} → {end_time}\n") 709 | else: 710 | print("Time range: Not used (entire video)\n") 711 | 712 | # Menu options 713 | options = [ 714 | "Enable/Disable time range" + (" [Enabled]" if config['use_time_range'] else " [Disabled]"), 715 | "Set start point", 716 | "Set end point", 717 | "Use entire video (reset range)" 718 | ] 719 | 720 | choice = print_menu("Time range options:", options) 721 | 722 | if choice == '0': 723 | return config 724 | elif choice == '1': 725 | # Enable/Disable range 726 | config['use_time_range'] = not config['use_time_range'] 727 | status = "enabled" if config['use_time_range'] else "disabled" 728 | print(f"\n✅ Time range {status}.") 729 | input("\nPress ENTER to continue...") 730 | elif choice == '2' and config['use_time_range']: 731 | # Set start point 732 | clear_screen() 733 | print_header() 734 | print(f"SET START POINT - {video_filename}\n") 735 | print(f"Total video duration: {duration_str}") 736 | print(f"Format: HH:MM:SS or MM:SS\n") 737 | 738 | new_value = input("New start point (leave empty for start of video): ") 739 | 740 | if new_value: 741 | try: 742 | seconds = time_to_seconds(new_value) 743 | # Check if the time is valid 744 | if seconds >= 0: 745 | if seconds < duration: 746 | config['start_time'] = new_value 747 | print(f"\n✅ Start point set: {new_value} ({format_time(seconds)})") 748 | 749 | # Update end point if necessary 750 | end_seconds = time_to_seconds(config['end_time']) if config['end_time'] else float('inf') 751 | if end_seconds < seconds: 752 | config['end_time'] = "" 753 | print(f"⚠️ End point reset to end of video (was before start point)") 754 | else: 755 | print(f"\n⚠️ Start point is beyond video duration ({duration_str}).") 756 | else: 757 | print("\n⚠️ Time must be positive.") 758 | except ValueError: 759 | print("\n⚠️ Invalid time format. Use HH:MM:SS or MM:SS.") 760 | else: 761 | config['start_time'] = "00:00:00" 762 | print("\n✅ Start point reset to beginning of video.") 763 | 764 | input("\nPress ENTER to continue...") 765 | elif choice == '3' and config['use_time_range']: 766 | # Set end point 767 | clear_screen() 768 | print_header() 769 | print(f"SET END POINT - {video_filename}\n") 770 | print(f"Total video duration: {duration_str}") 771 | print(f"Current start point: {config['start_time']}") 772 | print(f"Format: HH:MM:SS or MM:SS\n") 773 | 774 | new_value = input("New end point (leave empty for end of video): ") 775 | 776 | if new_value: 777 | try: 778 | seconds = time_to_seconds(new_value) 779 | # Check if the time is valid 780 | if seconds >= 0: 781 | # Check if end point is after start point 782 | start_seconds = time_to_seconds(config['start_time']) 783 | if seconds > start_seconds: 784 | config['end_time'] = new_value 785 | print(f"\n✅ End point set: {new_value} ({format_time(seconds)})") 786 | if seconds > duration: 787 | print(f"⚠️ Note: End point is beyond video duration ({duration_str}), end of video will be used.") 788 | else: 789 | print(f"\n⚠️ End point must be after start point ({config['start_time']}).") 790 | else: 791 | print("\n⚠️ Time must be positive.") 792 | except ValueError: 793 | print("\n⚠️ Invalid time format. Use HH:MM:SS or MM:SS.") 794 | else: 795 | config['end_time'] = "" 796 | print("\n✅ End point reset to end of video.") 797 | 798 | input("\nPress ENTER to continue...") 799 | elif choice == '4': 800 | # Reset range (use entire video) 801 | config['use_time_range'] = False 802 | config['start_time'] = "00:00:00" 803 | config['end_time'] = "" 804 | print("\n✅ Time range reset (entire video).") 805 | input("\nPress ENTER to continue...") 806 | else: 807 | print("\n⚠️ Invalid choice. Try again.") 808 | input("\nPress ENTER to continue...") 809 | 810 | # Return to time range menu 811 | return menu_time_range(video_path, config) 812 | 813 | def main_menu(): 814 | """Main application menu""" 815 | # Check GPU at startup 816 | ensure_gpu_packages() 817 | 818 | config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json') 819 | config = load_config(config_file) 820 | 821 | while True: 822 | clear_screen() 823 | print_header() 824 | 825 | # Find all videos 826 | current_dir = os.path.dirname(os.path.abspath(__file__)) 827 | video_files = find_video_files(current_dir) 828 | 829 | if not video_files: 830 | print("❌ No video files found in the current directory.") 831 | print("Please add video files (mp4, mkv, avi, mov, wmv) in the same folder as the script.") 832 | input("\nPress ENTER to exit...") 833 | sys.exit(1) 834 | 835 | print("Videos found in the folder:") 836 | for i, file in enumerate(video_files): 837 | print(f"{i+1}. {file}") 838 | 839 | choice = input("\nSelect a video (1-{}) or 'q' to exit: ".format(len(video_files))) 840 | 841 | if choice.lower() == 'q': 842 | print("\nGoodbye!") 843 | sys.exit(0) 844 | 845 | try: 846 | idx = int(choice) - 1 847 | if 0 <= idx < len(video_files): 848 | selected_video = video_files[idx] 849 | video_path = os.path.join(current_dir, selected_video) 850 | 851 | # Menu for the selected video 852 | menu_video(video_path, config, config_file) 853 | else: 854 | print("\n⚠️ Invalid choice. Try again.") 855 | input("\nPress ENTER to continue...") 856 | except ValueError: 857 | print("\n⚠️ Enter a valid number.") 858 | input("\nPress ENTER to continue...") 859 | 860 | def menu_video(video_path, config, config_file): 861 | """Menu for the selected video""" 862 | video_filename = os.path.basename(video_path) 863 | 864 | while True: 865 | options = [ 866 | f"Start extraction with default parameters", 867 | f"Customize parameters", 868 | f"Set time range", 869 | f"View parameter descriptions", 870 | f"Reset to default parameters" 871 | ] 872 | 873 | choice = print_menu(f"Selected video: {video_filename}", options) 874 | 875 | if choice == '0': 876 | return 877 | elif choice == '1': 878 | # Start with default parameters 879 | extract_frames_from_scenes(video_path, config) 880 | input("\nPress ENTER to return to menu...") 881 | elif choice == '2': 882 | # Customize parameters 883 | config = menu_customize_parameters(video_path, config, config_file) 884 | elif choice == '3': 885 | # Set time range 886 | config = menu_time_range(video_path, config) 887 | save_config(config, config_file) 888 | elif choice == '4': 889 | # View parameter descriptions 890 | menu_parameter_descriptions() 891 | elif choice == '5': 892 | # Reset to default parameters 893 | config = DEFAULT_CONFIG.copy() 894 | save_config(config, config_file) 895 | print("\n✅ Parameters reset to default values.") 896 | input("\nPress ENTER to continue...") 897 | else: 898 | print("\n⚠️ Invalid choice. Try again.") 899 | input("\nPress ENTER to continue...") 900 | 901 | def menu_customize_parameters(video_path, config, config_file): 902 | """Menu for customizing parameters""" 903 | while True: 904 | options = [ 905 | f"Maximum number of frames [{config['max_frames']}]", 906 | f"Sharpness search window [{config['sharpness_window']}]", 907 | f"GPU usage [{'Enabled' if config['use_gpu'] else 'Disabled'}]", 908 | f"Frame distribution [{'Proportional' if config['distribution_method'] == 'proportional' else 'Fixed'}]", 909 | f"Frames per 10 seconds [{config['frames_per_10s']}]", 910 | f"Max frames per scene [{config['max_frames_per_scene']}]", 911 | f"Output format [{config['output_format']}]", 912 | f"JPG quality [{config['jpg_quality']}]", 913 | f"Output directory [{config['output_dir']}]", 914 | f"Scene detection threshold [{config['scene_threshold']}]", 915 | f"GPU batch size [{config['batch_size']}]", 916 | f"Start with these parameters" 917 | ] 918 | 919 | choice = print_menu("CUSTOMIZE PARAMETERS", options) 920 | 921 | if choice == '0': 922 | # Save configuration and go back 923 | save_config(config, config_file) 924 | return config 925 | elif choice == '1': 926 | # Maximum number of frames 927 | try: 928 | new_value = int(input("\nEnter new value for maximum number of frames: ")) 929 | if new_value > 0: 930 | config['max_frames'] = new_value 931 | print(f"\n✅ Value updated: {new_value}") 932 | else: 933 | print("\n⚠️ Value must be greater than 0.") 934 | except ValueError: 935 | print("\n⚠️ Enter a valid number.") 936 | elif choice == '2': 937 | # Sharpness search window 938 | try: 939 | new_value = int(input("\nEnter new value for sharpness search window: ")) 940 | if new_value > 0: 941 | config['sharpness_window'] = new_value 942 | print(f"\n✅ Value updated: {new_value}") 943 | else: 944 | print("\n⚠️ Value must be greater than 0.") 945 | except ValueError: 946 | print("\n⚠️ Enter a valid number.") 947 | elif choice == '3': 948 | # GPU usage 949 | if not CUDA_AVAILABLE: 950 | print("\n⚠️ GPU not available on your system.") 951 | input("\nPress ENTER to continue...") 952 | continue 953 | 954 | gpu_choice = input("\nEnable GPU acceleration? (y/n): ").lower() 955 | if gpu_choice in ['y', 'yes']: 956 | config['use_gpu'] = True 957 | print("\n✅ GPU acceleration enabled.") 958 | elif gpu_choice in ['n', 'no']: 959 | config['use_gpu'] = False 960 | print("\n✅ GPU acceleration disabled.") 961 | else: 962 | print("\n⚠️ Invalid choice.") 963 | elif choice == '4': 964 | # Distribution method 965 | dist_choice = input("\nChoose distribution method (p=proportional, f=fixed): ").lower() 966 | if dist_choice in ['p', 'proportional']: 967 | config['distribution_method'] = 'proportional' 968 | print("\n✅ Proportional distribution set.") 969 | elif dist_choice in ['f', 'fixed']: 970 | config['distribution_method'] = 'fixed' 971 | print("\n✅ Fixed distribution set.") 972 | else: 973 | print("\n⚠️ Invalid choice.") 974 | elif choice == '5': 975 | # Frames per 10 seconds 976 | try: 977 | new_value = float(input("\nEnter number of frames to extract every 10 seconds: ")) 978 | if new_value > 0: 979 | config['frames_per_10s'] = new_value 980 | print(f"\n✅ Value updated: {new_value}") 981 | else: 982 | print("\n⚠️ Value must be greater than 0.") 983 | except ValueError: 984 | print("\n⚠️ Enter a valid number.") 985 | elif choice == '6': 986 | # Max frames per scene 987 | try: 988 | new_value = int(input("\nEnter maximum number of frames per scene: ")) 989 | if new_value > 0: 990 | config['max_frames_per_scene'] = new_value 991 | print(f"\n✅ Value updated: {new_value}") 992 | else: 993 | print("\n⚠️ Value must be greater than 0.") 994 | except ValueError: 995 | print("\n⚠️ Enter a valid number.") 996 | elif choice == '7': 997 | # Output format 998 | format_choice = input("\nChoose output format (jpg/png): ").lower() 999 | if format_choice in ['jpg', 'jpeg']: 1000 | config['output_format'] = 'jpg' 1001 | print("\n✅ JPG format set.") 1002 | elif format_choice in ['png']: 1003 | config['output_format'] = 'png' 1004 | print("\n✅ PNG format set.") 1005 | else: 1006 | print("\n⚠️ Invalid format. Use jpg or png.") 1007 | elif choice == '8': 1008 | # JPG quality 1009 | try: 1010 | new_value = int(input("\nEnter JPG quality (1-100): ")) 1011 | if 1 <= new_value <= 100: 1012 | config['jpg_quality'] = new_value 1013 | print(f"\n✅ Quality set: {new_value}") 1014 | else: 1015 | print("\n⚠️ Quality must be between 1 and 100.") 1016 | except ValueError: 1017 | print("\n⚠️ Enter a valid number.") 1018 | elif choice == '9': 1019 | # Output directory 1020 | new_value = input("\nEnter output directory: ") 1021 | if new_value: 1022 | config['output_dir'] = new_value 1023 | print(f"\n✅ Directory set: {new_value}") 1024 | else: 1025 | print("\n⚠️ Invalid directory.") 1026 | elif choice == '10': 1027 | # Scene detection threshold 1028 | try: 1029 | new_value = float(input("\nEnter scene detection threshold (1-100, lower values=more scenes): ")) 1030 | if 1 <= new_value <= 100: 1031 | config['scene_threshold'] = new_value 1032 | print(f"\n✅ Threshold set: {new_value}") 1033 | else: 1034 | print("\n⚠️ Threshold must be between 1 and 100.") 1035 | except ValueError: 1036 | print("\n⚠️ Enter a valid number.") 1037 | elif choice == '11': 1038 | # GPU batch size 1039 | if not CUDA_AVAILABLE: 1040 | print("\n⚠️ GPU not available. Setting ignored.") 1041 | input("\nPress ENTER to continue...") 1042 | continue 1043 | 1044 | try: 1045 | new_value = int(input("\nEnter GPU batch size (1-10): ")) 1046 | if 1 <= new_value <= 10: 1047 | config['batch_size'] = new_value 1048 | print(f"\n✅ Batch size set: {new_value}") 1049 | else: 1050 | print("\n⚠️ Size must be between 1 and 10.") 1051 | except ValueError: 1052 | print("\n⚠️ Enter a valid number.") 1053 | elif choice == '12': 1054 | # Start with these parameters 1055 | save_config(config, config_file) 1056 | extract_frames_from_scenes(video_path, config) 1057 | input("\nPress ENTER to return to menu...") 1058 | else: 1059 | print("\n⚠️ Invalid choice. Try again.") 1060 | input("\nPress ENTER to continue...") 1061 | 1062 | # Save configuration 1063 | save_config(config, config_file) 1064 | 1065 | return config 1066 | 1067 | def menu_parameter_descriptions(): 1068 | """Show detailed parameter descriptions""" 1069 | clear_screen() 1070 | print_header() 1071 | 1072 | print("PARAMETER DESCRIPTIONS\n") 1073 | 1074 | print("1. Maximum number of frames") 1075 | print(" Determines how many frames to extract in total from the video.") 1076 | print(" Higher values = larger dataset, but more processing time.") 1077 | print(" Recommended: 2000-5000 for most videos.") 1078 | 1079 | print("\n2. Sharpness search window") 1080 | print(" Number of frames before and after to analyze to find the sharpest frame.") 1081 | print(" Higher values = sharper frames, but more processing time.") 1082 | print(" Recommended: 5-10 to balance quality and speed.") 1083 | 1084 | print("\n3. GPU usage") 1085 | print(" Enables/disables GPU acceleration for image processing operations.") 1086 | print(" Recommended: Enabled if a compatible NVIDIA GPU is available.") 1087 | 1088 | print("\n4. Frame distribution") 1089 | print(" 'Proportional': extracts more frames for long scenes, fewer for short scenes.") 1090 | print(" 'Fixed': extracts the same number of frames for each scene.") 1091 | print(" Recommended: Proportional for better coverage.") 1092 | 1093 | print("\n5. Frames per 10 seconds") 1094 | print(" How many frames to extract every 10 seconds of scene (in proportional mode).") 1095 | print(" Higher values = more frames for long scenes.") 1096 | print(" Recommended: 1-2 for most cases.") 1097 | 1098 | print("\n6. Max frames per scene") 1099 | print(" Upper limit of frames that can be extracted from a single scene.") 1100 | print(" Recommended: 5-10 to avoid too many similar images.") 1101 | 1102 | print("\n7. Output format") 1103 | print(" 'jpg': more compact, slightly lower quality.") 1104 | print(" 'png': better quality, larger files.") 1105 | print(" Recommended: jpg for most cases.") 1106 | 1107 | print("\n8. JPG quality") 1108 | print(" Quality/compression level for JPG files (1-100).") 1109 | print(" Higher values = better quality, larger files.") 1110 | print(" Recommended: 85-95 for a good balance.") 1111 | 1112 | print("\n9. Output directory") 1113 | print(" The folder where extracted frames will be saved.") 1114 | print(" A subfolder with the video name is automatically created.") 1115 | 1116 | print("\n10. Scene detection threshold") 1117 | print(" Sensitivity in detecting scene changes (1-100).") 1118 | print(" Lower values = more scenes detected.") 1119 | print(" Recommended: 25-35 for standard videos, 15-25 for videos with many quick cuts.") 1120 | 1121 | print("\n11. GPU batch size") 1122 | print(" Number of frames to process simultaneously with the GPU.") 1123 | print(" Higher values = more speed, but more GPU memory required.") 1124 | print(" Recommended: 4-8 for GPUs with 8GB+ memory, 2-4 for GPUs with less memory.") 1125 | 1126 | print("\n12. Time range") 1127 | print(" Defines a specific portion of the video to analyze.") 1128 | print(" Useful for skipping opening/closing credits or focusing on specific scenes.") 1129 | print(" Set through the 'Set time range' menu.") 1130 | 1131 | input("\nPress ENTER to return to previous menu...") 1132 | 1133 | if __name__ == "__main__": 1134 | try: 1135 | main_menu() 1136 | except KeyboardInterrupt: 1137 | print("\n\nExecution interrupted by user.") 1138 | except Exception as e: 1139 | print(f"\n\nUnexpected error: {str(e)}") 1140 | input("\nPress ENTER to exit...") 1141 | -------------------------------------------------------------------------------- /LEGGIMI.md: -------------------------------------------------------------------------------- 1 | # Estrattore Immagini 2 | 3 | Questo programma consente di estrarre automaticamente fotogrammi di alta qualità da file video per creare dataset di addestramento per modelli LoRA. Il programma divide automaticamente il video in scene, seleziona i fotogrammi più nitidi e li salva in formato immagine. 4 | 5 | ## Caratteristiche Principali 6 | 7 | - **Rilevamento automatico delle scene**: Identifica i cambi di scena nel video 8 | - **Selezione intelligente dei fotogrammi**: Sceglie i frame più nitidi per ogni scena 9 | - **Accelerazione GPU**: Supporto per CUDA per elaborazione più veloce (opzionale) 10 | - **Interfaccia intuitiva**: Menu interattivo a riga di comando 11 | - **Configurazione personalizzabile**: Regola tutti i parametri per adattarli alle tue esigenze 12 | 13 | ## Requisiti di Sistema 14 | 15 | - Python 3.7 o superiore 16 | - OpenCV 17 | - PyTorch (per accelerazione GPU) 18 | - SceneDetect 19 | - Altri pacchetti Python (elencati in requirements.txt) 20 | - NVIDIA GPU con driver compatibili (opzionale, per accelerazione GPU) 21 | 22 | ## Installazione 23 | 24 | ### 1. Clona o scarica questo repository: 25 | 26 | ```bash 27 | git clone https://github.com/Tranchillo/Frame_Extractor.git 28 | cd Frame_Extractor 29 | ``` 30 | 31 | ### 2. Creazione di un ambiente virtuale (consigliato) 32 | 33 | È consigliabile utilizzare un ambiente virtuale Python per evitare conflitti con altre installazioni: 34 | 35 | ```bash 36 | # Creazione dell'ambiente virtuale 37 | python -m venv venv 38 | 39 | # Attivazione dell'ambiente virtuale 40 | # Su Windows: 41 | venv\Scripts\activate 42 | # Su macOS/Linux: 43 | source venv/bin/activate 44 | ``` 45 | 46 | Una volta attivato l'ambiente virtuale, dovresti vedere `(venv)` all'inizio della riga di comando, indicando che stai lavorando nell'ambiente isolato. 47 | 48 | ### 3. Installazione delle dipendenze 49 | 50 | #### Verifica se hai una GPU NVIDIA disponibile 51 | 52 | Esegui il comando `nvidia-smi` per verificare se hai una GPU NVIDIA e quali driver sono installati: 53 | 54 | ```bash 55 | nvidia-smi 56 | ``` 57 | 58 | Se il comando funziona, dovresti vedere un output simile a questo: 59 | 60 | ``` 61 | +-----------------------------------------------------------------------------+ 62 | | NVIDIA-SMI 535.146.02 Driver Version: 535.146.02 CUDA Version: 12.2 | 63 | | ... | 64 | ``` 65 | 66 | Prendi nota della versione CUDA riportata (nell'esempio è 12.2). 67 | 68 | #### Installazione delle dipendenze di base 69 | 70 | Installa le dipendenze principali utilizzando il file requirements.txt: 71 | 72 | ```bash 73 | pip install -r requirements.txt 74 | ``` 75 | 76 | #### Installazione PyTorch con supporto CUDA 77 | 78 | In base alla versione CUDA mostrata da `nvidia-smi`, scegli il comando di installazione corretto: 79 | 80 | - Per CUDA 11.8: 81 | 82 | ```bash 83 | pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 84 | ``` 85 | 86 | - Per CUDA 12.1: 87 | 88 | ```bash 89 | pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 90 | ``` 91 | 92 | - Se non hai una GPU o preferisci usare solo CPU: 93 | 94 | ```bash 95 | pip install torch torchvision torchaudio 96 | ``` 97 | 98 | ## Utilizzo 99 | 100 | ### Avvio del programma 101 | 102 | Posiziona i file video (.mp4, .mkv, .avi, ecc.) nella stessa cartella del programma, quindi esegui: 103 | 104 | ```bash 105 | # Assicurati che l'ambiente virtuale sia attivato 106 | # Su Windows: 107 | venv\Scripts\activate 108 | # Su macOS/Linux: 109 | source venv/bin/activate 110 | 111 | # Avvia il programma 112 | python Estrattore_Immagini.py 113 | ``` 114 | 115 | ### Menu principale 116 | 117 | All'avvio, il programma visualizzerà un menu con i video disponibili nella cartella corrente. Seleziona un numero per scegliere il video da elaborare. 118 | 119 | ### Menu del video selezionato 120 | 121 | Dopo aver selezionato un video, apparirà un menu con le seguenti opzioni: 122 | 123 | 1. **Avvia estrazione con parametri predefiniti**: Inizia subito l'estrazione dei frame 124 | 2. **Personalizza parametri**: Modifica i parametri di estrazione 125 | 3. **Imposta intervallo temporale**: Scegli una porzione specifica del video 126 | 4. **Visualizza descrizione dei parametri**: Informazioni dettagliate sui parametri 127 | 5. **Ripristina parametri predefiniti**: Reimposta tutte le impostazioni 128 | 129 | ### Parametri personalizzabili 130 | 131 | Tutti i parametri possono essere regolati in base alle tue esigenze: 132 | 133 | - **Numero massimo di frame**: Quanti frame estrarre in totale 134 | - **Finestra ricerca nitidezza**: Per selezionare i frame più nitidi 135 | - **Utilizzo GPU**: Attiva/disattiva l'accelerazione hardware 136 | - **Distribuzione frame**: Proporzionale o fissa per ogni scena 137 | - **Frame ogni 10 secondi**: Densità di campionamento per scene lunghe 138 | - **Max frame per scena**: Limite per evitare troppe immagini simili 139 | - **Formato output**: JPG o PNG 140 | - **Qualità JPG**: Livello di compressione per i file JPG 141 | - **Directory output**: Dove salvare i frame estratti 142 | - **Soglia rilevamento scene**: Sensibilità nel rilevare i cambi di scena 143 | - **Dimensione batch GPU**: Per ottimizzare l'elaborazione parallela 144 | 145 | ### Intervallo temporale 146 | 147 | Puoi anche impostare un intervallo temporale specifico per concentrarti su una parte particolare del video: 148 | 149 | 1. **Attiva/disattiva intervallo temporale**: Abilita l'uso di un intervallo 150 | 2. **Imposta punto di inizio**: In formato HH:MM:SS 151 | 3. **Imposta punto di fine**: In formato HH:MM:SS 152 | 4. **Usa intero video**: Reimposta per utilizzare tutto il video 153 | 154 | ## Output 155 | 156 | I frame estratti vengono salvati nella directory specificata (predefinita: `frame_estratti`) in una sottocartella con il nome del file video. Ogni frame è nominato con il numero della scena e il timestamp. 157 | 158 | ## Suggerimenti per ottenere risultati migliori 159 | 160 | 1. **Impostazioni per video di alta qualità**: 161 | - Aumenta il numero massimo di frame a 3000-5000 162 | - Usa la distribuzione proporzionale 163 | - Imposta una finestra di ricerca nitidezza più ampia (7-10) 164 | 165 | 2. **Impostazioni per prestazioni veloci**: 166 | - Riduci il numero massimo di frame a 1000-2000 167 | - Attiva l'accelerazione GPU se disponibile 168 | - Usa una finestra di ricerca nitidezza più piccola (3-5) 169 | 170 | 3. **Estrazione di scene specifiche**: 171 | - Usa l'opzione "Imposta intervallo temporale" 172 | - Specifica i punti esatti di inizio e fine in formato HH:MM:SS 173 | 174 | ## Risoluzione dei problemi 175 | 176 | ### Problemi GPU 177 | 178 | Se riscontri problemi con l'accelerazione GPU: 179 | 180 | 1. **Verifica la compatibilità**: Assicurati di aver installato PyTorch con la versione CUDA corretta per i tuoi driver 181 | 2. **Disattiva l'accelerazione GPU**: Se i problemi persistono, puoi sempre usare la modalità CPU 182 | 3. **Aggiorna i driver**: A volte potrebbe essere necessario aggiornare i driver NVIDIA 183 | 184 | ### Errori di memoria 185 | 186 | Se il programma va in errore per problemi di memoria: 187 | 188 | 1. **Riduci la dimensione batch GPU**: Prova con valori più bassi 189 | 2. **Elabora meno frame**: Diminuisci il numero massimo di frame 190 | 3. **Processa un intervallo più piccolo**: Usa l'opzione intervallo temporale per elaborare il video in parti 191 | 192 | ## Versione inglese 193 | 194 | È disponibile anche una versione in inglese del programma: `Frame_Extractor.py`. Funziona esattamente allo stesso modo ma con tutti i menu e i messaggi in inglese. 195 | 196 | ## Licenza 197 | 198 | Questo software è distribuito con licenza MIT. 199 | 200 | --- 201 | 202 | Per domande, suggerimenti o segnalazioni di bug, apri un issue su GitHub: https://github.com/Tranchillo/Frame_Extractor/issues 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Dany Tranchillo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frame Extractor 2 | 3 | This program automatically extracts high-quality frames from video files to create training datasets for LoRA models. The program automatically divides the video into scenes, selects the sharpest frames, and saves them in image format. 4 | 5 | ## Key Features 6 | 7 | - **Automatic scene detection**: Identifies scene changes in the video 8 | - **Intelligent frame selection**: Chooses the sharpest frames for each scene 9 | - **GPU acceleration**: CUDA support for faster processing (optional) 10 | - **Intuitive interface**: Interactive command-line menu 11 | - **Customizable configuration**: Adjust all parameters to suit your needs 12 | 13 | ## System Requirements 14 | 15 | - Python 3.7 or higher 16 | - OpenCV 17 | - PyTorch (for GPU acceleration) 18 | - SceneDetect 19 | - Other Python packages (listed in requirements.txt) 20 | - NVIDIA GPU with compatible drivers (optional, for GPU acceleration) 21 | 22 | ## Installation 23 | 24 | ### 1. Clone or download this repository: 25 | 26 | ```bash 27 | git clone https://github.com/Tranchillo/Frame_Extractor.git 28 | cd Frame_Extractor 29 | ``` 30 | 31 | ### 2. Creating a virtual environment (recommended) 32 | 33 | It's recommended to use a Python virtual environment to avoid conflicts with other installations: 34 | 35 | ```bash 36 | # Create the virtual environment 37 | python -m venv venv 38 | 39 | # Activate the virtual environment 40 | # On Windows: 41 | venv\Scripts\activate 42 | # On macOS/Linux: 43 | source venv/bin/activate 44 | ``` 45 | 46 | Once the virtual environment is activated, you should see `(venv)` at the beginning of the command line, indicating that you're working in the isolated environment. 47 | 48 | ### 3. Installing dependencies 49 | 50 | #### Check if you have an NVIDIA GPU available 51 | 52 | Run the `nvidia-smi` command to check if you have an NVIDIA GPU and which drivers are installed: 53 | 54 | ```bash 55 | nvidia-smi 56 | ``` 57 | 58 | If the command works, you should see output similar to this: 59 | 60 | ``` 61 | +-----------------------------------------------------------------------------+ 62 | | NVIDIA-SMI 535.146.02 Driver Version: 535.146.02 CUDA Version: 12.2 | 63 | | ... | 64 | ``` 65 | 66 | Take note of the CUDA version reported (in the example it's 12.2). 67 | 68 | #### Installing basic dependencies 69 | 70 | Install the main dependencies using the requirements.txt file: 71 | 72 | ```bash 73 | pip install -r requirements.txt 74 | ``` 75 | 76 | #### Installing PyTorch with CUDA support 77 | 78 | Based on the CUDA version shown by `nvidia-smi`, choose the correct installation command: 79 | 80 | - For CUDA 11.8: 81 | 82 | ```bash 83 | pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 84 | ``` 85 | 86 | - For CUDA 12.1: 87 | 88 | ```bash 89 | pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 90 | ``` 91 | 92 | - If you don't have a GPU or prefer to use CPU only: 93 | 94 | ```bash 95 | pip install torch torchvision torchaudio 96 | ``` 97 | 98 | ## Usage 99 | 100 | ### Starting the program 101 | 102 | Place the video files (.mp4, .mkv, .avi, etc.) in the same folder as the program, then run: 103 | 104 | ```bash 105 | # Make sure the virtual environment is activated 106 | # On Windows: 107 | venv\Scripts\activate 108 | # On macOS/Linux: 109 | source venv/bin/activate 110 | 111 | # Start the program 112 | python Frame_Extractor.py 113 | ``` 114 | 115 | ### Main menu 116 | 117 | At startup, the program will display a menu with the available videos in the current folder. Select a number to choose the video to process. 118 | 119 | ### Selected video menu 120 | 121 | After selecting a video, a menu will appear with the following options: 122 | 123 | 1. **Start extraction with default parameters**: Begin frame extraction immediately 124 | 2. **Customize parameters**: Modify extraction parameters 125 | 3. **Set time range**: Choose a specific portion of the video 126 | 4. **View parameter descriptions**: Detailed information about parameters 127 | 5. **Reset to default parameters**: Reset all settings 128 | 129 | ### Customizable Parameters 130 | 131 | All parameters can be adjusted based on your needs: 132 | 133 | - **Maximum number of frames**: How many frames to extract in total 134 | - **Sharpness search window**: To select the sharpest frames 135 | - **GPU usage**: Enable/disable hardware acceleration 136 | - **Frame distribution**: Proportional or fixed for each scene 137 | - **Frames per 10 seconds**: Sampling density for long scenes 138 | - **Max frames per scene**: Limit to avoid too many similar images 139 | - **Output format**: JPG or PNG 140 | - **JPG quality**: Compression level for JPG files 141 | - **Output directory**: Where to save the extracted frames 142 | - **Scene detection threshold**: Sensitivity in detecting scene changes 143 | - **GPU batch size**: To optimize parallel processing 144 | 145 | ### Time Range 146 | 147 | You can also set a specific time range to focus on a particular part of the video: 148 | 149 | 1. **Enable/disable time range**: Enable the use of a range 150 | 2. **Set start point**: In HH:MM:SS format 151 | 3. **Set end point**: In HH:MM:SS format 152 | 4. **Use entire video**: Reset to use the entire video 153 | 154 | ## Output 155 | 156 | The extracted frames are saved in the specified directory (default: `extracted_frames`) in a subfolder with the video file name. Each frame is named with the scene number and timestamp. 157 | 158 | ## Tips for Better Results 159 | 160 | 1. **Settings for high-quality videos**: 161 | - Increase the maximum number of frames to 3000-5000 162 | - Use proportional distribution 163 | - Set a wider sharpness search window (7-10) 164 | 165 | 2. **Settings for fast performance**: 166 | - Reduce the maximum number of frames to 1000-2000 167 | - Enable GPU acceleration if available 168 | - Use a smaller sharpness search window (3-5) 169 | 170 | 3. **Extracting specific scenes**: 171 | - Use the "Set time range" option 172 | - Specify the exact start and end points in HH:MM:SS format 173 | 174 | ## Troubleshooting 175 | 176 | ### GPU Issues 177 | 178 | If you encounter problems with GPU acceleration: 179 | 180 | 1. **Check compatibility**: Make sure you installed PyTorch with the correct CUDA version for your drivers 181 | 2. **Disable GPU acceleration**: If problems persist, you can always use CPU mode 182 | 3. **Update drivers**: Sometimes you may need to update your NVIDIA drivers 183 | 184 | ### Memory Errors 185 | 186 | If the program crashes due to memory problems: 187 | 188 | 1. **Reduce GPU batch size**: Try lower values 189 | 2. **Process fewer frames**: Decrease the maximum number of frames 190 | 3. **Process a smaller range**: Use the time range option to process the video in parts 191 | 192 | ## Italian Version 193 | 194 | An Italian version of the program is also available: `Estrattore_Immagini.py`. It works exactly the same way but with all menus and messages in Italian. 195 | 196 | ## License 197 | 198 | This software is distributed under the MIT license. 199 | 200 | --- 201 | 202 | For questions, suggestions, or bug reports, open an issue on GitHub: https://github.com/Tranchillo/Frame_Extractor/issues 203 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.8 2 | colorama==0.4.6 3 | numpy==1.26.4 4 | opencv-python==4.11.0.86 5 | pillow==11.2.1 6 | scenedetect==0.6.2 7 | tqdm==4.67.1 --------------------------------------------------------------------------------