From 6a85c3ef1bdf8890f2f07592279387335af238b0 Mon Sep 17 00:00:00 2001 From: jouel88 Date: Fri, 15 May 2026 10:55:59 +0700 Subject: [PATCH] update --- services/extract_service.py | 156 ++++++++++-------------------------- 1 file changed, 44 insertions(+), 112 deletions(-) diff --git a/services/extract_service.py b/services/extract_service.py index 8dbef06..35d4ca4 100644 --- a/services/extract_service.py +++ b/services/extract_service.py @@ -5,95 +5,40 @@ import numpy as np import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) -from utils.lbp_features import preprocess_face, FACE_SIZE - -BLUR_THRESHOLD = 30.0 -MIN_FACE_RATIO = 0.15 +from utils.lbp_features import FACE_SIZE face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) -def get_blur_score(img): - return cv2.Laplacian(img, cv2.CV_64F).var() - - -def detect_frontal_face(gray_frame): - h, w = gray_frame.shape - min_size = int(min(h, w) * MIN_FACE_RATIO) - - faces = face_cascade.detectMultiScale( - gray_frame, scaleFactor=1.1, minNeighbors=5, - minSize=(min_size, min_size) - ) - - if len(faces) == 0: - faces = face_cascade.detectMultiScale( - gray_frame, scaleFactor=1.05, minNeighbors=4, - minSize=(min_size, min_size) - ) - - if len(faces) == 0: - return None - - faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True) - return faces[0] - - -def estimate_yaw_from_symmetry(gray_face): - h, w = gray_face.shape - mid = w // 2 - - left_half = gray_face[:, :mid].astype(np.float64) - right_half = cv2.flip(gray_face[:, mid:], 1).astype(np.float64) - - min_w = min(left_half.shape[1], right_half.shape[1]) - left_half = left_half[:, :min_w] - right_half = right_half[:, :min_w] - - if left_half.size == 0 or right_half.size == 0: - return 999.0 - - left_mean = np.mean(left_half) - right_mean = np.mean(right_half) - - diff = abs(left_mean - right_mean) - estimated_yaw = diff * 0.8 - - return estimated_yaw - - -def process_frame(frame, gray_frame): - face_rect = detect_frontal_face(gray_frame) - if face_rect is None: - return None, None, "no_face", 0.0, 0.0 - - (x, y, w, h) = face_rect +def crop_wajah(gray): + """Crop wajah persis seperti notebook ablasi jalur 2.""" + wajah = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(50, 50)) + if len(wajah) == 0: + return None, None + (x, y, w, h) = max(wajah, key=lambda r: r[2] * r[3]) sisi = max(w, h) cx, cy = x + w // 2, y + h // 2 half = sisi // 2 y1 = max(0, cy - half) - y2 = min(gray_frame.shape[0], cy + half) + y2 = min(gray.shape[0], cy + half) x1 = max(0, cx - half) - x2 = min(gray_frame.shape[1], cx + half) + x2 = min(gray.shape[1], cx + half) - face_crop_gray = gray_frame[y1:y2, x1:x2] - face_crop_color = frame[y1:y2, x1:x2] - - face_resized = cv2.resize(face_crop_gray, FACE_SIZE, interpolation=cv2.INTER_AREA) - - blur_score = get_blur_score(face_resized) - if blur_score < BLUR_THRESHOLD: - return None, None, f"blur ({round(blur_score, 1)})", blur_score, 0.0 - - yaw_estimate = estimate_yaw_from_symmetry(face_resized) - - return face_resized, face_crop_color, "ok", blur_score, yaw_estimate + face_crop_gray = cv2.resize(gray[y1:y2, x1:x2], FACE_SIZE) + face_crop_color = None + return face_crop_gray, (x1, y1, x2, y2) -def extract_frames(video_path, output_dir, target_frames=30): +def extract_frames(video_path, output_dir, target_frames=200): + """ + Ekstraksi frame sesuai ablasi jalur 2: + 1. Baca SEMUA frame + 2. Crop SEMUA frame (Haar Cascade) + 3. Sampling merata (np.linspace) dari yang berhasil crop + """ if not os.path.exists(video_path): raise Exception(f"Video tidak ditemukan: {video_path}") @@ -114,11 +59,9 @@ def extract_frames(video_path, output_dir, target_frames=30): if total_frames <= 0: raise Exception("Video tidak valid (0 frame).") - candidates = [] - frame_idx = 0 + semua_crop = [] skipped_no_face = 0 - skipped_blur = 0 - skipped_side = 0 + frame_idx = 0 while True: ret, frame = cap.read() @@ -126,54 +69,42 @@ def extract_frames(video_path, output_dir, target_frames=30): break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - face_img, face_color, status, blur_score, yaw = process_frame(frame, gray) + face_gray, bbox = crop_wajah(gray) - if face_img is None: - if "blur" in status: - skipped_blur += 1 - else: - skipped_no_face += 1 + if face_gray is None: + skipped_no_face += 1 frame_idx += 1 continue - candidates.append({ + (x1, y1, x2, y2) = bbox + face_color = frame[y1:y2, x1:x2] + + semua_crop.append({ 'frame_idx': frame_idx, - 'face_gray': face_img, + 'face_gray': face_gray, 'face_color': face_color, - 'blur_score': blur_score, - 'yaw': yaw }) frame_idx += 1 cap.release() - if len(candidates) == 0: + total_crop = len(semua_crop) + + if total_crop == 0: raise Exception( - "Tidak ada frame wajah frontal berkualitas yang berhasil diekstrak. " - "Pastikan wajah menghadap depan dengan pencahayaan cukup." + "Tidak ada frame wajah yang berhasil diekstrak. " + "Pastikan wajah terlihat jelas di video." ) - candidates.sort(key=lambda c: c['blur_score'], reverse=True) - - if len(candidates) > target_frames: - candidates_by_idx = sorted(candidates, key=lambda c: c['frame_idx']) - - chunk_size = len(candidates_by_idx) // target_frames - selected = [] - - for i in range(target_frames): - start = i * chunk_size - end = min(start + chunk_size, len(candidates_by_idx)) - chunk = candidates_by_idx[start:end] - - best_in_chunk = max(chunk, key=lambda c: c['blur_score']) - selected.append(best_in_chunk) - - candidates = sorted(selected, key=lambda c: c['frame_idx']) + if total_crop <= target_frames: + hasil = semua_crop + else: + indices = np.linspace(0, total_crop - 1, target_frames, dtype=int) + hasil = [semua_crop[i] for i in indices] saved_count = 0 - for cand in candidates: + for cand in hasil: filename = f"frame_{saved_count:03d}.jpg" filename_raw = f"raw_frame_{saved_count:03d}.jpg" cv2.imwrite( @@ -184,14 +115,15 @@ def extract_frames(video_path, output_dir, target_frames=30): cv2.imwrite(os.path.join(output_dir, filename_raw), cand['face_color']) saved_count += 1 + interval = total_crop / saved_count if saved_count > 0 else 0 + return { "total_video_frames": total_frames, "video_fps": round(fps, 1), - "total_candidates": len(candidates), + "total_crop_detected": total_crop, "total_extracted": saved_count, "target_frames": target_frames, "skipped_no_face": skipped_no_face, - "skipped_blur": skipped_blur, - "skipped_side_face": skipped_side, + "sampling_interval": round(interval, 1), "output_dir": output_dir }