From 54b6216f2d5df951d85b28b652ea6d783c5d3bc8 Mon Sep 17 00:00:00 2001 From: jouel88 Date: Thu, 14 May 2026 19:50:49 +0700 Subject: [PATCH] finall --- app.py | 93 ++++++++++++++- config.py | 3 +- services/embedding_service.py | 214 ++++++++++++++++++++++++++++++++++ services/extract_service.py | 12 +- services/verify_service.py | 12 +- 5 files changed, 321 insertions(+), 13 deletions(-) create mode 100644 services/embedding_service.py diff --git a/app.py b/app.py index 6397738..0dff9e5 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,11 @@ from config import DATASETS_DIR, MODELS_DIR, TEMP_DIR, MAX_CONTENT_LENGTH, API_K from services.extract_service import extract_frames from services.train_service import train_model from services.verify_service import verify_face +from services.embedding_service import ( + compute_embedding_from_frames, + compute_embedding_from_image, + compute_embedding_from_video +) logging.basicConfig( level=logging.INFO, @@ -108,7 +113,6 @@ def api_train_model(): logger.info( f"Training selesai. Users: {result['total_users']}, " - f"CV: {result['cv_score']*100:.2f}%, " f"Test: {result['test_accuracy']*100:.2f}%" ) @@ -117,7 +121,6 @@ def api_train_model(): "message": ( f"Model SVM berhasil dilatih. " f"{result['total_users']} user, " - f"CV={result['cv_score']*100:.2f}%, " f"Test={result['test_accuracy']*100:.2f}%" ), **result @@ -171,6 +174,92 @@ def api_verify_face(): os.remove(temp_file) +@app.route('/extract-and-embed', methods=['POST']) +@require_api_key +def api_extract_and_embed(): + if 'video' not in request.files: + return jsonify({"status": "error", "message": "File video tidak ditemukan."}), 400 + + user_id = request.form.get('user_id') + target_frames = int(request.form.get('target_frames', 200)) + + if not user_id: + return jsonify({"status": "error", "message": "user_id wajib diisi."}), 400 + + video_file = request.files['video'] + temp_video = os.path.join(TEMP_DIR, f"enroll_{user_id}_{uuid.uuid4().hex[:8]}.mp4") + + try: + video_file.save(temp_video) + + output_dir = os.path.join(DATASETS_DIR, str(user_id)) + + result = extract_frames(temp_video, output_dir, target_frames=target_frames) + + embedding = compute_embedding_from_frames(output_dir) + + logger.info( + f"Extract & embed berhasil untuk user {user_id}. " + f"Frames: {result['total_extracted']}, " + f"Embedding dim: {len(embedding)}" + ) + + return jsonify({ + "status": "success", + "message": f"Berhasil mengekstrak {result['total_extracted']} frame dan menghitung embedding", + "embedding": embedding, + **result + }) + + except Exception as e: + logger.error(f"Extract & embed GAGAL untuk user {user_id}: {str(e)}") + return jsonify({"status": "error", "message": str(e)}), 500 + + finally: + if os.path.exists(temp_video): + os.remove(temp_video) + + +@app.route('/get-embedding', methods=['POST']) +@require_api_key +def api_get_embedding(): + if 'file' not in request.files: + return jsonify({"status": "error", "message": "File tidak ditemukan."}), 400 + + is_video = request.form.get('is_video', 'false').lower() == 'true' + + uploaded_file = request.files['file'] + ext = 'mp4' if is_video else 'jpg' + temp_file = os.path.join(TEMP_DIR, f"embed_{uuid.uuid4().hex[:8]}.{ext}") + + try: + uploaded_file.save(temp_file) + + if is_video: + embedding, blur_score = compute_embedding_from_video(temp_file, target_frames=10) + else: + embedding, blur_score = compute_embedding_from_image(temp_file) + + logger.info( + f"Get embedding berhasil. " + f"Dim: {len(embedding)}, blur: {round(blur_score, 1)}" + ) + + return jsonify({ + "status": "success", + "embedding": embedding, + "blur_score": round(blur_score, 1), + }) + + except Exception as e: + logger.error(f"Get embedding GAGAL: {str(e)}") + return jsonify({"status": "error", "message": str(e)}), 500 + + finally: + if os.path.exists(temp_file): + os.remove(temp_file) + + if __name__ == '__main__': logger.info("=" * 50) logger.info("MPG HRIS - Flask ML API Server") diff --git a/config.py b/config.py index 16fb5e7..8b1ba97 100644 --- a/config.py +++ b/config.py @@ -3,7 +3,8 @@ import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) STORAGE_DIR = os.path.join(BASE_DIR, 'storage') -DATASETS_DIR = os.path.join(STORAGE_DIR, 'face_datasets') +LARAVEL_STORAGE = os.path.join(BASE_DIR, '..', 'storage', 'app', 'private') +DATASETS_DIR = os.path.join(LARAVEL_STORAGE, 'face_datasets') MODELS_DIR = os.path.join(STORAGE_DIR, 'face_models') TEMP_DIR = os.path.join(STORAGE_DIR, 'temp') diff --git a/services/embedding_service.py b/services/embedding_service.py new file mode 100644 index 0000000..6febeb4 --- /dev/null +++ b/services/embedding_service.py @@ -0,0 +1,214 @@ +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 diff --git a/services/extract_service.py b/services/extract_service.py index ac3e1ad..8dbef06 100644 --- a/services/extract_service.py +++ b/services/extract_service.py @@ -71,11 +71,13 @@ def process_frame(frame, gray_frame): (x, y, w, h) = face_rect - padding = int(max(w, h) * 0.1) - x1 = max(0, x - padding) - y1 = max(0, y - padding) - x2 = min(gray_frame.shape[1], x + w + padding) - y2 = min(gray_frame.shape[0], y + h + padding) + 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) + x1 = max(0, cx - half) + x2 = min(gray_frame.shape[1], cx + half) face_crop_gray = gray_frame[y1:y2, x1:x2] face_crop_color = frame[y1:y2, x1:x2] diff --git a/services/verify_service.py b/services/verify_service.py index 56b3bb8..1cd2abb 100644 --- a/services/verify_service.py +++ b/services/verify_service.py @@ -80,11 +80,13 @@ def detect_and_crop_face(gray): 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) + sisi = max(fw, fh) + cx, cy = x + fw // 2, y + fh // 2 + half = sisi // 2 + y1 = max(0, cy - half) + y2 = min(h, cy + half) + x1 = max(0, cx - half) + x2 = min(w, cx + half) face_crop = gray[y1:y2, x1:x2]