import cv2 import os import sys import logging import numpy as np logger = logging.getLogger('flask_ml') sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from utils.lbp_features import extract_lbp_features, preprocess_face, FACE_SIZE BLUR_THRESHOLD = 30.0 face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_frontalface_default.xml' ) profile_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + 'haarcascade_profileface.xml' ) def get_blur_score(img): return cv2.Laplacian(img, cv2.CV_64F).var() def detect_and_crop_face(gray): h, w = gray.shape if max(h, w) > 640: scale = 640.0 / max(h, w) gray = cv2.resize(gray, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA) h, w = gray.shape min_size = int(min(h, w) * 0.1) faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(min_size, min_size)) if len(faces) == 0: faces = face_cascade.detectMultiScale(gray, 1.05, 3, minSize=(min_size, min_size)) if len(faces) == 0: faces = profile_cascade.detectMultiScale(gray, 1.1, 3, minSize=(min_size, min_size)) if len(faces) == 0: flipped = cv2.flip(gray, 1) faces = face_cascade.detectMultiScale(flipped, 1.05, 3, minSize=(min_size, min_size)) if len(faces) > 0: gray = flipped if len(faces) == 0: return None faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True) (x, y, fw, fh) = faces[0] padding = int(max(fw, fh) * 0.1) x1 = max(0, x - padding) y1 = max(0, y - padding) x2 = min(w, x + fw + padding) y2 = min(h, y + fh + padding) face_crop = gray[y1:y2, x1:x2] if face_crop.shape[0] < 10 or face_crop.shape[1] < 10: return None face_resized = cv2.resize(face_crop, FACE_SIZE, interpolation=cv2.INTER_AREA) return face_resized def compute_embedding_from_frames(frame_dir): valid_ext = ('.jpg', '.jpeg', '.png') features_list = [] files = sorted([ f for f in os.listdir(frame_dir) if f.lower().endswith(valid_ext) and f.startswith('frame_') ]) if len(files) == 0: raise Exception("Tidak ada frame wajah di folder dataset.") for filename in files: filepath = os.path.join(frame_dir, filename) img = cv2.imread(filepath, cv2.IMREAD_GRAYSCALE) if img is None: continue proc = preprocess_face(img) features = extract_lbp_features(proc) features_list.append(features) if len(features_list) == 0: raise Exception("Tidak ada frame valid untuk dihitung embedding-nya.") avg_embedding = np.mean(features_list, axis=0) norm = np.linalg.norm(avg_embedding) if norm > 0: avg_embedding = avg_embedding / norm return avg_embedding.tolist() def compute_embedding_from_image(image_path): from PIL import Image try: pil_img = Image.open(image_path) exif = pil_img.getexif() orientation = exif.get(274, 1) if orientation == 3: pil_img = pil_img.rotate(180, expand=True) elif orientation == 6: pil_img = pil_img.rotate(270, expand=True) elif orientation == 8: pil_img = pil_img.rotate(90, expand=True) img_array = np.array(pil_img) if len(img_array.shape) == 3 and img_array.shape[2] == 3: img_array = cv2.cvtColor(img_array, cv2.COLOR_RGB2BGR) img = img_array except Exception: img = cv2.imread(image_path) if img is None: raise Exception("Gagal membaca file gambar.") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img face = detect_and_crop_face(gray) if face is None: raise Exception( "Wajah tidak terdeteksi. Pastikan pencahayaan cukup dan wajah terlihat jelas." ) blur_score = get_blur_score(face) if blur_score < BLUR_THRESHOLD: raise Exception( f"Foto terlalu buram (Score: {round(blur_score, 1)}). Harap foto ulang." ) proc = preprocess_face(face) features = extract_lbp_features(proc) norm = np.linalg.norm(features) if norm > 0: features = features / norm return features.tolist(), float(blur_score) def compute_embedding_from_video(video_path, target_frames=10): cap = cv2.VideoCapture(video_path) if not cap.isOpened(): raise Exception("Gagal membuka file video verifikasi.") total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) if total_frames <= 0: cap.release() raise Exception("Video tidak valid (0 frame).") candidates = [] frame_idx = 0 sample_interval = max(1, total_frames // (target_frames * 3)) while True: ret, frame = cap.read() if not ret: break if frame_idx % sample_interval != 0: frame_idx += 1 continue gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) face_raw = detect_and_crop_face(gray) if face_raw is None: frame_idx += 1 continue blur_score = get_blur_score(face_raw) if blur_score < BLUR_THRESHOLD: frame_idx += 1 continue proc = preprocess_face(face_raw) features = extract_lbp_features(proc) candidates.append({ 'features': features, 'blur_score': blur_score, }) frame_idx += 1 cap.release() if len(candidates) == 0: raise Exception( "Tidak ada frame wajah yang valid dalam video. " "Pastikan wajah menghadap depan dengan pencahayaan cukup." ) candidates.sort(key=lambda c: c['blur_score'], reverse=True) selected = candidates[:target_frames] features_list = [c['features'] for c in selected] avg_embedding = np.mean(features_list, axis=0) norm = np.linalg.norm(avg_embedding) if norm > 0: avg_embedding = avg_embedding / norm avg_blur = float(np.mean([c['blur_score'] for c in selected])) return avg_embedding.tolist(), avg_blur