520 lines
22 KiB
Plaintext
520 lines
22 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "899c35b3",
|
|
"metadata": {},
|
|
"source": [
|
|
"# 🎓 Evaluasi Kinerja Sistem Pengenalan Wajah Berbasis LBP dan SVM\n",
|
|
"\n",
|
|
"**Laporan Eksperimen Pengujian Biometrik Lanjut**\n",
|
|
"\n",
|
|
"Notebook ini merupakan panduan saintifik yang lengkap untuk demonstrasi uji coba *Computer Vision Traditional* LBP-SVM. Mencakup Analisa Komponen Vektor hingga Kurva Evaluasi Metrik Biometrik Standar Skripsi/Tesis.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "6c21ecf7",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import os\n",
|
|
"import sys\n",
|
|
"import json\n",
|
|
"import logging\n",
|
|
"import numpy as np\n",
|
|
"import pandas as pd\n",
|
|
"import joblib\n",
|
|
"import cv2\n",
|
|
"from PIL import Image\n",
|
|
"\n",
|
|
"import seaborn as sns\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import matplotlib.patches as patches\n",
|
|
"\n",
|
|
"from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay, roc_curve, auc\n",
|
|
"from sklearn.decomposition import PCA\n",
|
|
"import warnings\n",
|
|
"from sklearn.exceptions import InconsistentVersionWarning\n",
|
|
"warnings.filterwarnings(\"ignore\", category=InconsistentVersionWarning)\n",
|
|
"\n",
|
|
"try:\n",
|
|
" BASE_DIR = os.path.dirname(os.path.abspath(__file__)) \n",
|
|
"except NameError:\n",
|
|
" BASE_DIR = os.path.abspath('')\n",
|
|
"\n",
|
|
"if not os.path.exists(os.path.join(BASE_DIR, 'python_scripts')):\n",
|
|
" BASE_DIR = r\"C:\\xampp\\htdocs\\mpg-hris\"\n",
|
|
"\n",
|
|
"DATASET_DIR = os.path.join(BASE_DIR, 'storage', 'app', 'private', 'face_datasets')\n",
|
|
"MODEL_DIR = os.path.join(BASE_DIR, 'storage', 'app', 'private', 'face_models')\n",
|
|
"DEBUG_DIR = os.path.join(BASE_DIR, 'storage', 'app', 'private', 'debug_verify')\n",
|
|
"\n",
|
|
"DF_APPROVED = 1.5\n",
|
|
"DF_PENDING = 0.5\n",
|
|
"\n",
|
|
"scripts_path = os.path.join(BASE_DIR, 'python_scripts')\n",
|
|
"if scripts_path not in sys.path:\n",
|
|
" sys.path.insert(0, scripts_path)\n",
|
|
" \n",
|
|
"from lbp_features import extract_lbp_features, preprocess_face, FACE_SIZE, _lbp_uniform\n",
|
|
"\n",
|
|
"face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')\n",
|
|
"profile_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_profileface.xml')\n",
|
|
"print(\"✅ Lingkungan eksekusi Akademik Advanced berhasil dimuat.\")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "961e72b4",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 1. Arsitektur Klasifikasi: Load Model Multi-Class SVM\n",
|
|
"* **StandardScaler**: Mengubah mean pada dataset fitur LBP (1792 dimensi) ke nilai 0 berdeviasi 1.\n",
|
|
"* **SVM Decision Function**: Kami menggunakan kernel probabilistik margin L2 dengan evaluasi absolut jarak Hyperplane.\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "d7a07d5f",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def load_system_model():\n",
|
|
" svm_path = os.path.join(MODEL_DIR, 'face_model.pkl')\n",
|
|
" scaler_path = os.path.join(MODEL_DIR, 'face_scaler.pkl')\n",
|
|
" labels_path = os.path.join(MODEL_DIR, 'face_labels.json')\n",
|
|
" svm_m = joblib.load(svm_path)\n",
|
|
" scaler_m = joblib.load(scaler_path)\n",
|
|
" with open(labels_path, 'r') as f:\n",
|
|
" lbl = json.load(f)\n",
|
|
" return svm_m, scaler_m, lbl\n",
|
|
"\n",
|
|
"svm, scaler, labels = load_system_model()\n",
|
|
"kelas_terdaftar = [str(c) for c in svm.classes_ if str(c) != 'unknown']\n",
|
|
"\n",
|
|
"print(f\"✅ Model klasifikasi berhasil diload ke memory. Identitas Aktif: {kelas_terdaftar}\")\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "56d2305c",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 2. API Pipeline Wajah dan Crop Geometrik"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "8213583b",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def detect_and_crop_face(gray):\n",
|
|
" h, w = gray.shape\n",
|
|
" if max(h, w) > 640:\n",
|
|
" scale = 640.0 / max(h, w)\n",
|
|
" gray = cv2.resize(gray, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)\n",
|
|
" h, w = gray.shape\n",
|
|
"\n",
|
|
" min_size = int(min(h, w) * 0.1)\n",
|
|
" faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(min_size, min_size))\n",
|
|
" if len(faces) == 0:\n",
|
|
" faces = face_cascade.detectMultiScale(gray, 1.05, 3, minSize=(min_size, min_size))\n",
|
|
" if len(faces) == 0:\n",
|
|
" faces = profile_cascade.detectMultiScale(gray, 1.1, 3, minSize=(min_size, min_size))\n",
|
|
"\n",
|
|
" if len(faces) == 0: return None\n",
|
|
" faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)\n",
|
|
" (x, y, fw, fh) = faces[0]\n",
|
|
" padding = int(max(fw, fh) * 0.1)\n",
|
|
" x1, y1 = max(0, x - padding), max(0, y - padding)\n",
|
|
" x2, y2 = min(w, x + fw + padding), min(h, y + fh + padding)\n",
|
|
" crop = gray[y1:y2, x1:x2]\n",
|
|
" return cv2.resize(crop, FACE_SIZE, interpolation=cv2.INTER_AREA)\n",
|
|
"\n",
|
|
"def process_image_file(img_path):\n",
|
|
" try:\n",
|
|
" pil_img = Image.open(img_path)\n",
|
|
" exif = pil_img.getexif()\n",
|
|
" orient = exif.get(274, 1)\n",
|
|
" if orient == 3: pil_img = pil_img.rotate(180, expand=True)\n",
|
|
" elif orient == 6: pil_img = pil_img.rotate(270, expand=True)\n",
|
|
" elif orient == 8: pil_img = pil_img.rotate(90, expand=True)\n",
|
|
" img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)\n",
|
|
" except Exception:\n",
|
|
" img = cv2.imread(img_path)\n",
|
|
" if img is None: return None, None, \"Fail\"\n",
|
|
" gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) if len(img.shape) == 3 else img\n",
|
|
" face = detect_and_crop_face(gray)\n",
|
|
" if face is None: return img, None, \"No Face\"\n",
|
|
" proc = preprocess_face(face)\n",
|
|
" feats = extract_lbp_features(proc)\n",
|
|
" return face, feats, \"OK\"\n",
|
|
"\n",
|
|
"def classify_secure(feats, expected_user_id):\n",
|
|
" classes = [str(c) for c in svm.classes_]\n",
|
|
" feats_s = scaler.transform([feats])\n",
|
|
" df_values = svm.decision_function(feats_s)[0]\n",
|
|
" if expected_user_id in classes:\n",
|
|
" idx = classes.index(expected_user_id)\n",
|
|
" user_df = float(df_values[idx] if hasattr(df_values, '__len__') else df_values)\n",
|
|
" else: user_df = -999.0\n",
|
|
"\n",
|
|
" df_sorted = sorted(enumerate(df_values), key=lambda x: -x[1])\n",
|
|
" predicted = str(classes[df_sorted[0][0]])\n",
|
|
" predicted_is_user = (predicted == expected_user_id)\n",
|
|
" if predicted_is_user and user_df >= DF_APPROVED: status = \"APPROVED\"\n",
|
|
" elif predicted_is_user and user_df >= DF_PENDING: status = \"PENDING\"\n",
|
|
" else: status = \"REJECTED\"\n",
|
|
" return {'predicted': predicted, 'user_df': user_df, 'status': status, 'match': status in (\"APPROVED\", \"PENDING\")}\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "3ef9c6fc",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 3. Visualisasi Anatomi Feature Extraction LBP Penuh"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "b3c69098",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"def detailed_visual_demo():\n",
|
|
" sample_file = None\n",
|
|
" target_user = None\n",
|
|
" for d in os.listdir(DATASET_DIR):\n",
|
|
" dir_path = os.path.join(DATASET_DIR, d)\n",
|
|
" if os.path.isdir(dir_path):\n",
|
|
" files = [f for f in os.listdir(dir_path) if f.startswith('raw_frame_') and f.endswith('.jpg')]\n",
|
|
" if files:\n",
|
|
" sample_file = os.path.join(dir_path, files[0])\n",
|
|
" target_user = d\n",
|
|
" break\n",
|
|
" if not sample_file: return\n",
|
|
"\n",
|
|
" raw_img = cv2.imread(sample_file)\n",
|
|
" raw_rgb = cv2.cvtColor(raw_img, cv2.COLOR_BGR2RGB)\n",
|
|
" gray = cv2.cvtColor(raw_img, cv2.COLOR_BGR2GRAY)\n",
|
|
" faces = face_cascade.detectMultiScale(gray, 1.1, 5, minSize=(30, 30))\n",
|
|
" x, y, w, h = (0,0,0,0)\n",
|
|
" if len(faces) > 0:\n",
|
|
" faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)\n",
|
|
" x, y, w, h = faces[0]\n",
|
|
"\n",
|
|
" face_raw, feats, msg = process_image_file(sample_file)\n",
|
|
" if face_raw is None: return\n",
|
|
" processed = preprocess_face(face_raw)\n",
|
|
" lbp_map = _lbp_uniform(processed, n_points=8, radius=1)\n",
|
|
" res = classify_secure(feats, expected_user_id=target_user)\n",
|
|
" \n",
|
|
" fig = plt.figure(figsize=(19, 11))\n",
|
|
" fig.suptitle(f\"Bedah Algoritma Vektor Wajah | Sampel Asli: User {target_user}\", fontsize=20, fontweight='bold', y=0.98)\n",
|
|
" \n",
|
|
" ax1 = plt.subplot(2, 3, 1)\n",
|
|
" ax1.imshow(raw_rgb)\n",
|
|
" if w > 0:\n",
|
|
" pad = int(max(w, h) * 0.1)\n",
|
|
" ax1.add_patch(patches.Rectangle((max(0, x-pad), max(0, y-pad)), w+(pad*2), h+(pad*2), linewidth=3, edgecolor='yellow', facecolor='none'))\n",
|
|
" ax1.set_title(\"1. Deteksi Wajah Geometris\", fontsize=13)\n",
|
|
" ax1.axis('off')\n",
|
|
" \n",
|
|
" ax2 = plt.subplot(2, 3, 2)\n",
|
|
" ax2.imshow(face_raw, cmap='gray')\n",
|
|
" ax2.set_title(\"2. Potongan Skala Abu 128px\", fontsize=13)\n",
|
|
" ax2.axis('off')\n",
|
|
" \n",
|
|
" ax3 = plt.subplot(2, 3, 3)\n",
|
|
" ax3.imshow(processed, cmap='gray')\n",
|
|
" ax3.set_title(\"3. Preprocessing Histogram CLAHE\", fontsize=13)\n",
|
|
" ax3.axis('off')\n",
|
|
" \n",
|
|
" ax4 = plt.subplot(2, 3, 4)\n",
|
|
" ax4.imshow(lbp_map, cmap='viridis')\n",
|
|
" ax4.set_title(\"4. Local Binary Map (LBP Texture)\", fontsize=13)\n",
|
|
" ax4.axis('off')\n",
|
|
" \n",
|
|
" ax5 = plt.subplot(2, 3, 5)\n",
|
|
" ax5.imshow(processed, cmap='gray')\n",
|
|
" step_x = 128 // 8\n",
|
|
" step_y = 128 // 8\n",
|
|
" for i in range(1, 8):\n",
|
|
" ax5.axhline(i*step_y, color='lime', linestyle='dashed', alpha=0.8)\n",
|
|
" ax5.axvline(i*step_x, color='lime', linestyle='dashed', alpha=0.8)\n",
|
|
" ax5.set_title(\"5. Peta 8x8 Lokalisasi Vektor\", fontsize=13)\n",
|
|
" ax5.axis('off')\n",
|
|
" \n",
|
|
" ax6 = plt.subplot(2, 3, 6)\n",
|
|
" ax6.bar(range(180), feats[:180], color='dodgerblue')\n",
|
|
" ax6.set_title(\"6. Output Vektor Array LBP 1D (SVM Feed)\", fontsize=13)\n",
|
|
" \n",
|
|
" plt.tight_layout()\n",
|
|
" plt.show()\n",
|
|
"\n",
|
|
"detailed_visual_demo()\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "e077bbaf",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 4. Evaluasi Akurasi Dasar (Confusion Matrix)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "4391500d",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"y_true = []\n",
|
|
"y_pred = []\n",
|
|
"\n",
|
|
"for uid in kelas_terdaftar:\n",
|
|
" folder = os.path.join(DATASET_DIR, uid)\n",
|
|
" files = sorted([f for f in os.listdir(folder) if f.startswith('raw_frame_') and f.endswith('.jpg')])\n",
|
|
" for fname in files:\n",
|
|
" face, feats, info = process_image_file(os.path.join(folder, fname))\n",
|
|
" if feats is not None:\n",
|
|
" result = classify_secure(feats, expected_user_id=uid)\n",
|
|
" y_true.append(uid)\n",
|
|
" y_pred.append(result['predicted'])\n",
|
|
"\n",
|
|
"labels_in_data = sorted(list(set(y_true + y_pred)))\n",
|
|
"cm = confusion_matrix(y_true, y_pred, labels=labels_in_data)\n",
|
|
"\n",
|
|
"fig, ax = plt.subplots(figsize=(8,6))\n",
|
|
"disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=labels_in_data)\n",
|
|
"disp.plot(cmap='Blues', ax=ax, values_format='d')\n",
|
|
"plt.title(\"Confusion Matrix Otentikasi SVM\", fontweight='bold', pad=20)\n",
|
|
"plt.show()\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "7f922126",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 5. Visualisasi Multivariabel 1792 Dimensi Jarak SVM (PCA 2D Cluster)\n",
|
|
"Jika anda bertanya *'Bagaimana cara SVM membedakan manusia satu dengan lainnya?'*, grafik ini jawabannya.\n",
|
|
"Sistem ini menggunakan algoritma dimensi **Principal Component Analysis (PCA)** untuk mereduksi `1792 Dimensi` ke 2 dimensi X dan Y agar bisa terlihat oleh kacamata penguji. \n",
|
|
"Apabila setiap klasifikasi identitas mengerucut dan mengumpul secara berkelompok (tidak berpencar atau meledak acak), maka model Machine Learning kita valid secara Geometrik."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "5e9f4ae7",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"X_data = [] # Array Matrix Fitur\n",
|
|
"y_labels = [] # Array Identitas Sah\n",
|
|
"genuine_scores = [] # Track skor keyakinan untuk label sebenarnya\n",
|
|
"impostor_scores = [] # Track invasi hacker ketika coba masuk account kita\n",
|
|
"\n",
|
|
"for uid in kelas_terdaftar:\n",
|
|
" folder = os.path.join(DATASET_DIR, uid)\n",
|
|
" files = sorted([f for f in os.listdir(folder) if f.startswith('raw_frame_') and f.endswith('.jpg')])\n",
|
|
" for fname in files:\n",
|
|
" face, feats, msg = process_image_file(os.path.join(folder, fname))\n",
|
|
" if feats is not None:\n",
|
|
" X_data.append(feats)\n",
|
|
" y_labels.append(uid)\n",
|
|
" \n",
|
|
" # --- Perhitungan Skor Jarak untuk Grafik Histogram Lonceng dan EER Nantinya ---\n",
|
|
" feats_s = scaler.transform([feats])\n",
|
|
" df_values = svm.decision_function(feats_s)[0]\n",
|
|
" for class_idx, predicted_uid in enumerate(svm.classes_):\n",
|
|
" predicted_uid = str(predicted_uid)\n",
|
|
" if predicted_uid == 'unknown': continue\n",
|
|
" \n",
|
|
" score = df_values[class_idx]\n",
|
|
" if predicted_uid == uid:\n",
|
|
" genuine_scores.append(score)\n",
|
|
" else:\n",
|
|
" impostor_scores.append(score)\n",
|
|
"\n",
|
|
"# --- MULAI PLOT PCA ---\n",
|
|
"print(\"Melakukan faktorisasi kompresi dimensi PCA...\")\n",
|
|
"pca = PCA(n_components=2)\n",
|
|
"components = pca.fit_transform(X_data)\n",
|
|
"\n",
|
|
"df_pca = pd.DataFrame(data = components, columns = ['Komponen LBP Skala Utama 1', 'Komponen LBP Skala Utama 2'])\n",
|
|
"df_pca['Identitas Ground Truth'] = y_labels\n",
|
|
"\n",
|
|
"plt.figure(figsize=(10, 7))\n",
|
|
"sns.scatterplot(\n",
|
|
" x='Komponen LBP Skala Utama 1', y='Komponen LBP Skala Utama 2',\n",
|
|
" hue='Identitas Ground Truth', data=df_pca, \n",
|
|
" palette=sns.color_palette(\"Set1\", len(kelas_terdaftar)),\n",
|
|
" s=120, alpha=0.9, edgecolor='white', linewidth=0.5\n",
|
|
")\n",
|
|
"plt.title('Pemetaan Relasi Fitur Wajah Manusia SVM (Clustering Visual)', fontsize=15, fontweight='bold', pad=15)\n",
|
|
"plt.grid(True, linestyle='--', alpha=0.6)\n",
|
|
"plt.legend(title='Database Pegawai', bbox_to_anchor=(1.05, 1), loc='upper left')\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "bc72d777",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 6. Histogram Lonceng Biometrik (*Genuine Score vs Impostor Score*)\n",
|
|
"Grafik penting otentikasi biometrik yang berfungsi melihat \"Jarak Jembatan\" (*Gap Wall*) ketebalan perlindungan Margin antara Wajah Pengguna Asli (*Genuine*) dan Penyusup Acak (*Impostor*).\n",
|
|
"Apabila Grafik Merah (*Impostor*) menembus dan bersinggungan erat menutupi bukit warna Hijau (*Authentic*), sistem tersebut dinilai rentan dan berbahaya!"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "ce954f49",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"plt.figure(figsize=(12, 6))\n",
|
|
"sns.histplot(genuine_scores, color='seagreen', label='Genuine Access (Orang Dalam Sah)', kde=True, stat=\"density\", bins=20, alpha=0.5, edgecolor=\"none\")\n",
|
|
"sns.histplot(impostor_scores, color='crimson', label='Impostor Access (Orang Lain Tersesat)', kde=True, stat=\"density\", bins=20, alpha=0.5, edgecolor=\"none\")\n",
|
|
"\n",
|
|
"plt.axvline(x=DF_APPROVED, color='gold', linestyle='--', linewidth=3, label=f'Margin Titik Kunci Server: DF({DF_APPROVED})')\n",
|
|
"\n",
|
|
"plt.title('Sebaran Probabilitas Decision Margin SVM Wajah', fontsize=15, fontweight='bold')\n",
|
|
"plt.xlabel('SVM Decision Margin Score / Angka Kepastian')\n",
|
|
"plt.ylabel('Kepadatan Distribusi Data (%)')\n",
|
|
"plt.legend(loc='upper right')\n",
|
|
"plt.grid(axis='y', linestyle='--', alpha=0.5)\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "20287582",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 7. Receiver Operating Characteristic (ROC) & Titik Evaluasi Otentikasi EER Ekstrim\n",
|
|
"- **Kurva Biru ROC (Gambar Kiri)**: Menggambarkan luasan area keakuratan (*Area Under Curve / AUC*), dinilai superlatif bilamana mendekati angka 1.00.\n",
|
|
"- **Titik Silang EER (Gambar Kanan)**: Adalah titik krusial persinggungan antara Resiko Alarm Palsu **(FAR)** dengan Resiko Salah Tolak Orang Nyata **(FRR)**. Secara akademis _Golden Standard Error Rate_ harus bernilai 0% ~ 1%. "
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "9b319996",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Label array: Asli (1) dan Palsu / Impostor (0)\n",
|
|
"y_binary_roc = [1] * len(genuine_scores) + [0] * len(impostor_scores)\n",
|
|
"y_scores_roc = genuine_scores + impostor_scores\n",
|
|
"\n",
|
|
"fpr_roc, tpr_roc, thresholds_roc = roc_curve(y_binary_roc, y_scores_roc)\n",
|
|
"roc_auc = auc(fpr_roc, tpr_roc)\n",
|
|
"\n",
|
|
"fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))\n",
|
|
"\n",
|
|
"# ---- 1. ROC CURVE ----\n",
|
|
"ax1.plot(fpr_roc, tpr_roc, color='dodgerblue', lw=3, label=f'Support Vector ROC (AUC = {roc_auc:.4f})')\n",
|
|
"ax1.plot([0, 1], [0, 1], color='darkgrey', lw=2, linestyle='--', label='Garis Lemparan Koin Acak (Keburukan)')\n",
|
|
"ax1.set_xlim([-0.05, 1.0])\n",
|
|
"ax1.set_ylim([0.0, 1.05])\n",
|
|
"ax1.set_xlabel('False Positive Rate (Rasio Kebocoran Penipu)', fontsize=12)\n",
|
|
"ax1.set_ylabel('True Positive Rate (Rasio Menyetujui Akun Sendiri)', fontsize=12)\n",
|
|
"ax1.set_title('Kesempurnaan Deteksi / ROC Curve', fontweight='bold', fontsize=14)\n",
|
|
"ax1.legend(loc='lower right')\n",
|
|
"ax1.grid(True, linestyle=':', alpha=0.7)\n",
|
|
"\n",
|
|
"# ---- 2. EER (EQUAL ERROR RATE) CURVE ----\n",
|
|
"# Formula Dasar Biometrik FRR adalah (100% - Tingkat Akseptasi Kebenaran TPR / False Rejection)\n",
|
|
"frr = 1 - tpr_roc\n",
|
|
"far = fpr_roc\n",
|
|
"\n",
|
|
"# Deteksi titik perpapasan/silang untuk mendapatkan Absolute Equal Error Rate (EER) Tengah\n",
|
|
"eer_index = np.argmin(np.abs(far - frr))\n",
|
|
"eer_threshold_val = thresholds_roc[eer_index]\n",
|
|
"eer_percent = far[eer_index]\n",
|
|
"\n",
|
|
"ax2.plot(thresholds_roc, far, color='crimson', lw=2.5, label='FAR (Orang Asing Diterima / Bahaya)')\n",
|
|
"ax2.plot(thresholds_roc, frr, color='navy', lw=2.5, label='FRR (Orang Asli Ditolak Mesin)')\n",
|
|
"ax2.axvline(x=eer_threshold_val, color='gold', linestyle='--', linewidth=2, label=f'EER={eer_percent*100:.2f}% (Toleransi Error Mutlak)\\nMargin Kritis di: {eer_threshold_val:.2f}')\n",
|
|
"\n",
|
|
"ax2.set_xlim([-1.5, 3.5]) # Memfokuskan grafik ke pergeseran persimpangan Decision Values\n",
|
|
"ax2.set_ylim([-0.05, 1.05])\n",
|
|
"ax2.set_xlabel('SVM Operational Decision Boundary Margin Limit', fontsize=12)\n",
|
|
"ax2.set_ylabel('Level Bahaya Error Tolerance', fontsize=12)\n",
|
|
"ax2.set_title('Kesetimbangan Resiko Operasional: FAR vs FRR', fontweight='bold', fontsize=14)\n",
|
|
"ax2.legend(loc='upper right')\n",
|
|
"ax2.grid(True, linestyle=':', alpha=0.7)\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()\n",
|
|
"\n",
|
|
"print(\"=\" * 60)\n",
|
|
"print(f\" >>> AREA UNDER CURVE KUALITAS SVM MURNI: {roc_auc:.4f} <<< \")\n",
|
|
"print(\" Titik AUC 1.000 memformulasikan Algoritma telah mengenali Identitas tanpa misklasifikasi sedetik pun di atas kertas.\")\n",
|
|
"print(f\" ✅ Equal Error Rate (EER) Absolut ditekan ke angka ekstrim: {eer_percent*100:.2f} % !!!\")\n",
|
|
"print(\"=\" * 60)\n"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "75690eb9",
|
|
"metadata": {},
|
|
"source": [
|
|
"## 8. Verifikasi Kasus Lintas Domain (Kondisi Lapangan Aplikasi Mobile)\n",
|
|
"Penyelesaian *Domain Gap* untuk mendiagnosa jepretan kamera Low Quality langsung dari Flutter Front-End Apps."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"id": "d4594f26",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"if not os.path.exists(DEBUG_DIR): print(\"Folder debug_verify log tidak ditemukan.\")\n",
|
|
"else:\n",
|
|
" debug_files = sorted([f for f in os.listdir(DEBUG_DIR) if f.endswith('.jpg') and f.startswith('user')])\n",
|
|
" \n",
|
|
" if len(debug_files) == 0: print(\"Berdasarkan pengumpulan lapangan belum ada data log dari Presensi App.\")\n",
|
|
" else:\n",
|
|
" print(f\"Ditemukan {len(debug_files)} Gambar Log Absen Harian Teruji Secara Domain Silang:\\n\")\n",
|
|
" print(f\"{'Nama File Uji':<32} {'Target':<7} {'Status':<10} {'Margin SVM':>10} {'Akurasi Jauh'}\")\n",
|
|
" print(\"-\" * 80)\n",
|
|
" \n",
|
|
" real_total = 0\n",
|
|
" real_correct = 0\n",
|
|
" for fname in debug_files:\n",
|
|
" expected_uid = fname.split('_')[0].replace('user', '')\n",
|
|
" fpath = os.path.join(DEBUG_DIR, fname)\n",
|
|
" face, feats, msg = process_image_file(fpath)\n",
|
|
" if feats is None: continue\n",
|
|
" r = classify_secure(feats, expected_user_id=expected_uid)\n",
|
|
" real_total += 1\n",
|
|
" if r['match']: real_correct += 1\n",
|
|
" \n",
|
|
" mar = \"✔️ Lolos Security\" if r['match'] else \"❌ Gagal (Wajah Ditolak/Noise)\"\n",
|
|
" print(f\"{fname:<32} User {expected_uid:<2} {r['status']:<10} {r['user_df']:>10.3f} {mar}\")\n",
|
|
" \n",
|
|
" print(\"-\" * 80)\n",
|
|
" print(f\"Keandalan Validasi Perbedaan Domain & Resolusi Ekstrem Kamera Mobile: {real_correct}/{real_total} ({(real_correct/real_total)*100 if real_total else 0:.1f}%)\")\n"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|