220 lines
7.2 KiB
Python
220 lines
7.2 KiB
Python
import cv2
|
|
import sys
|
|
import os
|
|
import json
|
|
import numpy as np
|
|
|
|
# --- KONFIGURASI & KONSTANTA ---
|
|
FACE_SIZE = (200, 200)
|
|
BLUR_THRESHOLD = 30.0 # Dilonggarkan untuk kamera depan HP
|
|
THRESHOLD_MATCH = 80.0 # < 80: Approved (langsung cocok)
|
|
THRESHOLD_REVIEW = 100.0 # 80-100: Pending (Review HRD)
|
|
|
|
# Inisialisasi Cascade
|
|
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
|
|
profile_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_profileface.xml')
|
|
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_eye.xml')
|
|
|
|
def apply_clahe(gray_img):
|
|
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
|
return clahe.apply(gray_img)
|
|
|
|
def get_blur_score(img):
|
|
return cv2.Laplacian(img, cv2.CV_64F).var()
|
|
|
|
def fix_exif_rotation(img_path):
|
|
"""Memperbaiki orientasi gambar berdasarkan EXIF metadata dari kamera HP"""
|
|
try:
|
|
from PIL import Image
|
|
pil_img = Image.open(img_path)
|
|
exif = pil_img.getexif()
|
|
orientation = exif.get(274, 1) # Tag 274 = Orientation
|
|
|
|
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)
|
|
return img_array
|
|
except ImportError:
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
def detect_faces_multi(gray, min_face_size):
|
|
"""Coba deteksi wajah di 4 orientasi (0, 90, 180, 270) + flip"""
|
|
rotations = [
|
|
(0, None),
|
|
(90, cv2.ROTATE_90_CLOCKWISE),
|
|
(180, cv2.ROTATE_180),
|
|
(270, cv2.ROTATE_90_COUNTERCLOCKWISE),
|
|
]
|
|
for angle, rot_code in rotations:
|
|
test_gray = gray if rot_code is None else cv2.rotate(gray, rot_code)
|
|
|
|
# Frontal face - strict
|
|
faces = face_cascade.detectMultiScale(test_gray, 1.1, 5, minSize=(min_face_size, min_face_size))
|
|
if len(faces) > 0:
|
|
return faces, test_gray
|
|
|
|
# Frontal face - longgar
|
|
faces = face_cascade.detectMultiScale(test_gray, 1.05, 3, minSize=(min_face_size, min_face_size))
|
|
if len(faces) > 0:
|
|
return faces, test_gray
|
|
|
|
# Flip horizontal
|
|
flipped = cv2.flip(test_gray, 1)
|
|
faces = face_cascade.detectMultiScale(flipped, 1.05, 3, minSize=(min_face_size, min_face_size))
|
|
if len(faces) > 0:
|
|
return faces, flipped
|
|
|
|
return [], gray
|
|
|
|
def align_face(gray_img, face_rect):
|
|
(x, y, w, h) = face_rect
|
|
roi_gray = gray_img[y:y+h, x:x+w]
|
|
|
|
eyes = eye_cascade.detectMultiScale(roi_gray, 1.1, 15, minSize=(w//6, h//6))
|
|
|
|
if len(eyes) >= 2:
|
|
eyes = sorted(eyes, key=lambda e: e[0])
|
|
l_center = (int(eyes[0][0] + eyes[0][2]/2), int(eyes[0][1] + eyes[0][3]/2))
|
|
r_center = (int(eyes[1][0] + eyes[1][2]/2), int(eyes[1][1] + eyes[1][3]/2))
|
|
|
|
dy = r_center[1] - l_center[1]
|
|
dx = r_center[0] - l_center[0]
|
|
angle = np.degrees(np.arctan2(dy, dx))
|
|
|
|
if abs(angle) < 30:
|
|
center = (float(w / 2), float(h / 2))
|
|
M = cv2.getRotationMatrix2D(center, angle, 1.0)
|
|
rotated = cv2.warpAffine(roi_gray, M, (w, h), flags=cv2.INTER_CUBIC)
|
|
return rotated
|
|
|
|
return roi_gray
|
|
|
|
def preprocess_image(img_path):
|
|
"""Pipeline pemrosesan gambar dengan EXIF rotation handling"""
|
|
if not os.path.exists(img_path):
|
|
raise Exception("File gambar input tidak ditemukan.")
|
|
|
|
# Coba baca dengan EXIF rotation fix dulu (kamera HP)
|
|
img = fix_exif_rotation(img_path)
|
|
if img is None:
|
|
img = cv2.imread(img_path)
|
|
|
|
if img is None:
|
|
raise Exception("Gagal membaca file gambar (Corrupted/Not Valid).")
|
|
|
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
|
|
# 1. Cek Kualitas: Blur
|
|
blur_score = get_blur_score(gray)
|
|
if blur_score < BLUR_THRESHOLD:
|
|
raise Exception(f"Foto terlalu buram (Score: {round(blur_score, 1)}). Harap foto ulang.")
|
|
|
|
h_img, w_img = gray.shape
|
|
min_face_size = int(min(h_img, w_img) * 0.1)
|
|
|
|
# 2. Deteksi Wajah (Multi-orientasi + flip)
|
|
faces, gray = detect_faces_multi(gray, min_face_size)
|
|
|
|
if len(faces) == 0:
|
|
# Terakhir coba profil samping
|
|
faces = profile_cascade.detectMultiScale(gray, 1.1, 3, minSize=(min_face_size, min_face_size))
|
|
|
|
if len(faces) == 0:
|
|
raise Exception("Wajah tidak terdeteksi. Pastikan pencahayaan cukup dan wajah terlihat jelas.")
|
|
|
|
# Ambil wajah terbesar
|
|
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
|
|
(x, y, w, h) = faces[0]
|
|
|
|
# 3. Cek Jarak
|
|
face_ratio = max(w, h) / max(h_img, w_img)
|
|
if face_ratio < 0.1:
|
|
raise Exception("Wajah terlalu jauh. Mohon dekatkan kamera.")
|
|
|
|
# 4. Alignment & Preprocessing
|
|
aligned_face = align_face(gray, faces[0])
|
|
resized_face = cv2.resize(aligned_face, FACE_SIZE, interpolation=cv2.INTER_AREA)
|
|
denoised = cv2.bilateralFilter(resized_face, 5, 75, 75)
|
|
final_img = apply_clahe(denoised)
|
|
|
|
return final_img, blur_score
|
|
|
|
def verify_face(model_path, image_path):
|
|
try:
|
|
if not os.path.exists(model_path):
|
|
raise Exception("Model biometrik user belum tersedia (Belum training).")
|
|
|
|
# Parameter HARUS sama dengan train_face.py
|
|
recognizer = cv2.face.LBPHFaceRecognizer_create(
|
|
radius=1,
|
|
neighbors=8,
|
|
grid_x=8,
|
|
grid_y=8,
|
|
threshold=100.0
|
|
)
|
|
recognizer.read(model_path)
|
|
|
|
processed_face = None
|
|
blur_score = 0.0
|
|
|
|
# Preprocess Image
|
|
try:
|
|
processed_face, blur_score = preprocess_image(image_path)
|
|
except Exception as e_proc:
|
|
print(json.dumps({
|
|
"status": "success",
|
|
"match": False,
|
|
"confidence": 999,
|
|
"verification_status": "PREPROCESSING_FAILED",
|
|
"message": str(e_proc)
|
|
}))
|
|
return
|
|
|
|
# Prediksi
|
|
id_user, confidence = recognizer.predict(processed_face)
|
|
|
|
# Logika Keputusan: LBPH 0 = Identik, >100 = Sangat Berbeda
|
|
status_verifikasi = "REJECTED"
|
|
is_match = False
|
|
|
|
if confidence < THRESHOLD_MATCH:
|
|
status_verifikasi = "APPROVED"
|
|
is_match = True
|
|
elif confidence < THRESHOLD_REVIEW:
|
|
status_verifikasi = "PENDING"
|
|
is_match = True
|
|
|
|
print(json.dumps({
|
|
"status": "success",
|
|
"match": is_match,
|
|
"verification_status": status_verifikasi,
|
|
"confidence": round(confidence, 2),
|
|
"blur_score": round(blur_score, 1),
|
|
"user_id": id_user
|
|
}))
|
|
|
|
except Exception as e:
|
|
print(json.dumps({
|
|
"status": "error",
|
|
"message": str(e)
|
|
}))
|
|
|
|
if __name__ == "__main__":
|
|
if len(sys.argv) < 3:
|
|
print(json.dumps({"status": "error", "message": "Invalid arguments"}))
|
|
sys.exit(1)
|
|
|
|
m_path = sys.argv[1]
|
|
i_path = sys.argv[2]
|
|
|
|
verify_face(m_path, i_path)
|