finall
This commit is contained in:
parent
1741696a08
commit
54b6216f2d
93
app.py
93
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.extract_service import extract_frames
|
||||||
from services.train_service import train_model
|
from services.train_service import train_model
|
||||||
from services.verify_service import verify_face
|
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(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|
@ -108,7 +113,6 @@ def api_train_model():
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Training selesai. Users: {result['total_users']}, "
|
f"Training selesai. Users: {result['total_users']}, "
|
||||||
f"CV: {result['cv_score']*100:.2f}%, "
|
|
||||||
f"Test: {result['test_accuracy']*100:.2f}%"
|
f"Test: {result['test_accuracy']*100:.2f}%"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -117,7 +121,6 @@ def api_train_model():
|
||||||
"message": (
|
"message": (
|
||||||
f"Model SVM berhasil dilatih. "
|
f"Model SVM berhasil dilatih. "
|
||||||
f"{result['total_users']} user, "
|
f"{result['total_users']} user, "
|
||||||
f"CV={result['cv_score']*100:.2f}%, "
|
|
||||||
f"Test={result['test_accuracy']*100:.2f}%"
|
f"Test={result['test_accuracy']*100:.2f}%"
|
||||||
),
|
),
|
||||||
**result
|
**result
|
||||||
|
|
@ -171,6 +174,92 @@ def api_verify_face():
|
||||||
os.remove(temp_file)
|
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__':
|
if __name__ == '__main__':
|
||||||
logger.info("=" * 50)
|
logger.info("=" * 50)
|
||||||
logger.info("MPG HRIS - Flask ML API Server")
|
logger.info("MPG HRIS - Flask ML API Server")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ import os
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
STORAGE_DIR = os.path.join(BASE_DIR, 'storage')
|
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')
|
MODELS_DIR = os.path.join(STORAGE_DIR, 'face_models')
|
||||||
TEMP_DIR = os.path.join(STORAGE_DIR, 'temp')
|
TEMP_DIR = os.path.join(STORAGE_DIR, 'temp')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -71,11 +71,13 @@ def process_frame(frame, gray_frame):
|
||||||
|
|
||||||
(x, y, w, h) = face_rect
|
(x, y, w, h) = face_rect
|
||||||
|
|
||||||
padding = int(max(w, h) * 0.1)
|
sisi = max(w, h)
|
||||||
x1 = max(0, x - padding)
|
cx, cy = x + w // 2, y + h // 2
|
||||||
y1 = max(0, y - padding)
|
half = sisi // 2
|
||||||
x2 = min(gray_frame.shape[1], x + w + padding)
|
y1 = max(0, cy - half)
|
||||||
y2 = min(gray_frame.shape[0], y + h + padding)
|
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_gray = gray_frame[y1:y2, x1:x2]
|
||||||
face_crop_color = frame[y1:y2, x1:x2]
|
face_crop_color = frame[y1:y2, x1:x2]
|
||||||
|
|
|
||||||
|
|
@ -80,11 +80,13 @@ def detect_and_crop_face(gray):
|
||||||
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
|
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
|
||||||
(x, y, fw, fh) = faces[0]
|
(x, y, fw, fh) = faces[0]
|
||||||
|
|
||||||
padding = int(max(fw, fh) * 0.1)
|
sisi = max(fw, fh)
|
||||||
x1 = max(0, x - padding)
|
cx, cy = x + fw // 2, y + fh // 2
|
||||||
y1 = max(0, y - padding)
|
half = sisi // 2
|
||||||
x2 = min(w, x + fw + padding)
|
y1 = max(0, cy - half)
|
||||||
y2 = min(h, y + fh + padding)
|
y2 = min(h, cy + half)
|
||||||
|
x1 = max(0, cx - half)
|
||||||
|
x2 = min(w, cx + half)
|
||||||
|
|
||||||
face_crop = gray[y1:y2, x1:x2]
|
face_crop = gray[y1:y2, x1:x2]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue