refactor: pisahkan flask_ml_api dari repo Laravel - hapus 14461 file dari tracking (venv, dataset, model, notebook)
|
|
@ -25,4 +25,11 @@ Homestead.yaml
|
|||
Thumbs.db
|
||||
_ide_helper.php
|
||||
.phpstorm.meta.php
|
||||
_ide_helper_models.php
|
||||
|
||||
# Flask ML API (repo terpisah)
|
||||
/flask_ml_api/
|
||||
|
||||
# Research notebooks & scripts
|
||||
*.ipynb
|
||||
/notebook_face_recognition.py
|
||||
|
|
|
|||
|
|
@ -1,519 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
|
|
@ -1,753 +0,0 @@
|
|||
<?php
|
||||
|
||||
// @formatter:off
|
||||
// phpcs:ignoreFile
|
||||
/**
|
||||
* A helper file for your Eloquent Models
|
||||
* Copy the phpDocs from this file to the correct Model,
|
||||
* And remove them from this file, to prevent double declarations.
|
||||
*
|
||||
* @author Barry vd. Heuvel <barryvdh@gmail.com>
|
||||
*/
|
||||
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property string $id_approval
|
||||
* @property string $id_surat
|
||||
* @property int $id_approver
|
||||
* @property string|null $id_ttd_approver
|
||||
* @property int $tahap 1=Manajer, 2=HRD
|
||||
* @property string $status
|
||||
* @property string|null $catatan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $approver
|
||||
* @property-read \App\Models\SuratIzin $suratIzin
|
||||
* @property-read \App\Models\TandaTangan|null $tandaTanganApprover
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereCatatan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereIdApproval($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereIdApprover($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereIdSurat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereIdTtdApprover($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereTahap($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ApprovalSurat whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ApprovalSurat extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $id_user
|
||||
* @property string|null $path_model_yml
|
||||
* @property int $is_verified 0:Pending, 1:Approved, 2:Rejected
|
||||
* @property string|null $last_updated
|
||||
* @property string|null $encoding_wajah
|
||||
* @property string $tanggal_latih
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereEncodingWajah($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereIsVerified($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereLastUpdated($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah wherePathModelYml($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereTanggalLatih($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DataWajah whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class DataWajah extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $id_penggunaan
|
||||
* @property int $id_poin_sumber
|
||||
* @property int $jumlah_diambil
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\PenggunaanPoin $penggunaan
|
||||
* @property-read \App\Models\PoinLembur $poinSumber
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin whereIdPenggunaan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin whereIdPoinSumber($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin whereJumlahDiambil($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|DetailPenggunaanPoin whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class DetailPenggunaanPoin extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_divisi
|
||||
* @property string $nama_divisi
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Divisi newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Divisi newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Divisi query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Divisi whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Divisi whereIdDivisi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Divisi whereNamaDivisi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Divisi whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Divisi extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $tanggal
|
||||
* @property string $keterangan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur whereKeterangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur whereTanggal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|HariLibur whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class HariLibur extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_jabatan
|
||||
* @property string $nama_jabatan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Jabatan newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Jabatan newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Jabatan query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Jabatan whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Jabatan whereIdJabatan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Jabatan whereNamaJabatan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Jabatan whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Jabatan extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_jadwal
|
||||
* @property int|null $id_user
|
||||
* @property string|null $tanggal
|
||||
* @property int|null $id_shift
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\PenggunaanPoin|null $penggunaanPoin
|
||||
* @property-read \App\Models\ShiftKerja|null $shift
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja whereIdJadwal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja whereIdShift($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja whereTanggal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JadwalKerja whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class JadwalKerja extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_jenis_izin
|
||||
* @property string $nama_izin
|
||||
* @property string|null $created_at
|
||||
* @property string|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisIzin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisIzin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisIzin query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisIzin whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisIzin whereIdJenisIzin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisIzin whereNamaIzin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisIzin whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class JenisIzin extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_kompensasi
|
||||
* @property string $nama_kompensasi
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisKompensasi newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisKompensasi newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisKompensasi query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisKompensasi whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisKompensasi whereIdKompensasi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisKompensasi whereNamaKompensasi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisKompensasi whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class JenisKompensasi extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_pengurangan
|
||||
* @property string $nama_pengurangan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\PenggunaanPoin> $penggunaanPoin
|
||||
* @property-read int|null $penggunaan_poin_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisPengurangan newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisPengurangan newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisPengurangan query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisPengurangan whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisPengurangan whereIdPengurangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisPengurangan whereNamaPengurangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|JenisPengurangan whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class JenisPengurangan extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_kantor
|
||||
* @property string $nama_kantor
|
||||
* @property string|null $alamat
|
||||
* @property numeric|null $latitude
|
||||
* @property numeric|null $longitude
|
||||
* @property int $radius
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
|
||||
* @property-read int|null $users_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereAlamat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereIdKantor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereLatitude($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereLongitude($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereNamaKantor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereRadius($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Kantor whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Kantor extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property string $id_lembur
|
||||
* @property int|null $id_user
|
||||
* @property \Illuminate\Support\Carbon $tanggal_lembur
|
||||
* @property string $jam_mulai
|
||||
* @property string $jam_selesai
|
||||
* @property int|null $durasi_menit
|
||||
* @property string|null $keterangan
|
||||
* @property int|null $jumlah_poin
|
||||
* @property int|null $id_kompensasi
|
||||
* @property int $id_status
|
||||
* @property string|null $alasan_penolakan
|
||||
* @property \Illuminate\Support\Carbon $tanggal_diajukan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\JenisKompensasi|null $kompensasi
|
||||
* @property-read \App\Models\StatusPengajuan $status
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereAlasanPenolakan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereDurasiMenit($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereIdKompensasi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereIdLembur($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereIdStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereJamMulai($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereJamSelesai($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereJumlahPoin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereKeterangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereTanggalDiajukan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereTanggalLembur($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Lembur whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Lembur extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property string $id_izin
|
||||
* @property int|null $id_user
|
||||
* @property string $tanggal_mulai
|
||||
* @property string $tanggal_selesai
|
||||
* @property int|null $id_jenis_izin
|
||||
* @property string|null $alasan
|
||||
* @property string|null $bukti_file
|
||||
* @property int $id_status
|
||||
* @property string|null $alasan_penolakan
|
||||
* @property string $tanggal_diajukan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\JenisIzin|null $jenisIzin
|
||||
* @property-read \App\Models\StatusPengajuan $statusPengajuan
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereAlasan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereAlasanPenolakan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereBuktiFile($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereIdIzin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereIdJenisIzin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereIdStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereTanggalDiajukan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereTanggalMulai($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereTanggalSelesai($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PengajuanIzin whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class PengajuanIzin extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property string $id_penggunaan
|
||||
* @property int|null $id_user
|
||||
* @property \Illuminate\Support\Carbon $tanggal_penggunaan
|
||||
* @property int|null $jumlah_poin
|
||||
* @property string|null $jam_masuk_custom
|
||||
* @property string|null $jam_pulang_custom
|
||||
* @property int|null $id_pengurangan
|
||||
* @property int $id_status
|
||||
* @property string|null $alasan_penolakan
|
||||
* @property \Illuminate\Support\Carbon $tanggal_diajukan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\JenisPengurangan|null $jenisPengurangan
|
||||
* @property-read \App\Models\StatusPengajuan $status
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereAlasanPenolakan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereIdPenggunaan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereIdPengurangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereIdStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereJamMasukCustom($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereJamPulangCustom($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereJumlahPoin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereTanggalDiajukan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereTanggalPenggunaan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PenggunaanPoin whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class PenggunaanPoin extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_pengumuman
|
||||
* @property string|null $judul
|
||||
* @property string|null $isi
|
||||
* @property \Illuminate\Support\Carbon|null $tanggal
|
||||
* @property int|null $dibuat_oleh
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User|null $pembuat
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman whereDibuatOleh($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman whereIdPengumuman($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman whereIsi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman whereJudul($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman whereTanggal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Pengumuman whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Pengumuman extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_permission
|
||||
* @property string $nama_permission
|
||||
* @property string $slug
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Role> $roles
|
||||
* @property-read int|null $roles_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission whereIdPermission($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission whereNamaPermission($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission whereSlug($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Permission whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Permission extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Poin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Poin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Poin query()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Poin extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_poin
|
||||
* @property int|null $id_user
|
||||
* @property int|null $jumlah_poin
|
||||
* @property int|null $sisa_poin
|
||||
* @property string|null $id_lembur
|
||||
* @property string|null $keterangan
|
||||
* @property \Illuminate\Support\Carbon $tanggal
|
||||
* @property \Illuminate\Support\Carbon|null $expired_at
|
||||
* @property bool|null $is_fully_used
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\Lembur|null $lembur
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereExpiredAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereIdLembur($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereIdPoin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereIsFullyUsed($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereJumlahPoin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereKeterangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereSisaPoin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereTanggal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|PoinLembur whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class PoinLembur extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_presensi
|
||||
* @property int|null $id_user
|
||||
* @property string $tanggal
|
||||
* @property string|null $jam_masuk
|
||||
* @property string|null $jam_pulang
|
||||
* @property numeric|null $lat_masuk
|
||||
* @property numeric|null $lon_masuk
|
||||
* @property numeric|null $lat_pulang
|
||||
* @property numeric|null $lon_pulang
|
||||
* @property string|null $foto_wajah_masuk
|
||||
* @property string|null $foto_wajah_pulang
|
||||
* @property int|null $id_status
|
||||
* @property string|null $alasan_telat
|
||||
* @property string|null $keterangan_pulang
|
||||
* @property string|null $waktu_terlambat
|
||||
* @property string|null $waktu_masuk_awal
|
||||
* @property string|null $waktu_pulang_awal
|
||||
* @property string|null $waktu_pulang_akhir
|
||||
* @property int $verifikasi_wajah
|
||||
* @property int $id_validasi
|
||||
* @property string|null $keterangan_luar_radius
|
||||
* @property string|null $alasan_penolakan
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property string|null $deleted_at
|
||||
* @property-read \App\Models\User|null $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereAlasanPenolakan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereAlasanTelat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereFotoWajahMasuk($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereFotoWajahPulang($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereIdPresensi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereIdStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereIdValidasi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereJamMasuk($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereJamPulang($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereKeteranganLuarRadius($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereKeteranganPulang($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereLatMasuk($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereLatPulang($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereLonMasuk($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereLonPulang($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereTanggal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereVerifikasiWajah($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereWaktuMasukAwal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereWaktuPulangAkhir($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereWaktuPulangAwal($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Presensi whereWaktuTerlambat($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Presensi extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_riwayat
|
||||
* @property int $id_user_1
|
||||
* @property int $id_jadwal_1
|
||||
* @property int $id_user_2
|
||||
* @property int $id_jadwal_2
|
||||
* @property string|null $keterangan
|
||||
* @property int $created_by
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $execAdmin
|
||||
* @property-read \App\Models\JadwalKerja $jadwal1
|
||||
* @property-read \App\Models\JadwalKerja $jadwal2
|
||||
* @property-read \App\Models\User $user1
|
||||
* @property-read \App\Models\User $user2
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereCreatedBy($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereIdJadwal1($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereIdJadwal2($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereIdRiwayat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereIdUser1($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereIdUser2($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereKeterangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|RiwayatTukarShift whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class RiwayatTukarShift extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_role
|
||||
* @property string $nama_role
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Permission> $permissions
|
||||
* @property-read int|null $permissions_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\User> $users
|
||||
* @property-read int|null $users_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereIdRole($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereNamaRole($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|Role whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Role extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_shift
|
||||
* @property string|null $nama_shift
|
||||
* @property string|null $jam_mulai
|
||||
* @property string|null $jam_selesai
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\JadwalKerja> $jadwalKerja
|
||||
* @property-read int|null $jadwal_kerja_count
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja whereIdShift($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja whereJamMulai($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja whereJamSelesai($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja whereNamaShift($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|ShiftKerja whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ShiftKerja extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id_status
|
||||
* @property string $nama_status
|
||||
* @property string|null $created_at
|
||||
* @property string|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|StatusPengajuan newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|StatusPengajuan newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|StatusPengajuan query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|StatusPengajuan whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|StatusPengajuan whereIdStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|StatusPengajuan whereNamaStatus($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|StatusPengajuan whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class StatusPengajuan extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property string $id_surat
|
||||
* @property string $id_izin
|
||||
* @property int $id_user
|
||||
* @property string $nomor_surat
|
||||
* @property string $isi_surat
|
||||
* @property string|null $id_ttd_pengaju
|
||||
* @property string $status_surat
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ApprovalSurat|null $approvalHrd
|
||||
* @property-read \App\Models\ApprovalSurat|null $approvalManajer
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\ApprovalSurat> $approvals
|
||||
* @property-read int|null $approvals_count
|
||||
* @property-read \App\Models\PengajuanIzin $pengajuanIzin
|
||||
* @property-read \App\Models\TandaTangan|null $tandaTanganPengaju
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereIdIzin($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereIdSurat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereIdTtdPengaju($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereIsiSurat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereNomorSurat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereStatusSurat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|SuratIzin whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class SuratIzin extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property string $id_tanda_tangan
|
||||
* @property int $id_user
|
||||
* @property string $file_ttd
|
||||
* @property int $is_active
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\User $user
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan active()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan whereFileTtd($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan whereIdTandaTangan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan whereIdUser($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan whereIsActive($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|TandaTangan whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class TandaTangan extends \Eloquent {}
|
||||
}
|
||||
|
||||
namespace App\Models{
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string|null $nik
|
||||
* @property string $nama_lengkap
|
||||
* @property string $email
|
||||
* @property string|null $no_telp
|
||||
* @property string|null $alamat
|
||||
* @property string|null $email_verified_at
|
||||
* @property string $password
|
||||
* @property int $sisa_cuti
|
||||
* @property string|null $foto
|
||||
* @property string|null $remember_token
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property int|null $id_kantor
|
||||
* @property int|null $id_divisi
|
||||
* @property int|null $id_jabatan
|
||||
* @property int $status_aktif
|
||||
* @property string|null $tgl_bergabung
|
||||
* @property int $is_face_registered
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read \App\Models\DataWajah|null $dataWajah
|
||||
* @property-read \App\Models\Divisi|null $divisi
|
||||
* @property-read \App\Models\Jabatan|null $jabatan
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\JadwalKerja> $jadwalKerja
|
||||
* @property-read int|null $jadwal_kerja_count
|
||||
* @property-read \App\Models\Kantor|null $kantor
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Lembur> $lemburs
|
||||
* @property-read int|null $lemburs_count
|
||||
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
|
||||
* @property-read int|null $notifications_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\Role> $roles
|
||||
* @property-read int|null $roles_count
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \Laravel\Sanctum\PersonalAccessToken> $tokens
|
||||
* @property-read int|null $tokens_count
|
||||
* @method static \Database\Factories\UserFactory factory($count = null, $state = [])
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User onlyTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereAlamat($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmail($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereEmailVerifiedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereFoto($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereIdDivisi($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereIdJabatan($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereIdKantor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereIsFaceRegistered($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereNamaLengkap($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereNik($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereNoTelp($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User wherePassword($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereRememberToken($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereSisaCuti($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereStatusAktif($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereTglBergabung($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withTrashed(bool $withTrashed = true)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder<static>|User withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class User extends \Eloquent {}
|
||||
}
|
||||
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
import os
|
||||
import uuid
|
||||
import logging
|
||||
from functools import wraps
|
||||
from flask import Flask, request, jsonify
|
||||
from flask_cors import CORS
|
||||
|
||||
from config import DATASETS_DIR, MODELS_DIR, TEMP_DIR, MAX_CONTENT_LENGTH, API_KEY
|
||||
from services.extract_service import extract_frames
|
||||
from services.train_service import train_model
|
||||
from services.verify_service import verify_face
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[%(asctime)s] %(levelname)s: %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
|
||||
CORS(app)
|
||||
|
||||
|
||||
def require_api_key(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
key = request.headers.get('X-API-Key', '')
|
||||
if key != API_KEY:
|
||||
return jsonify({"status": "error", "message": "API key tidak valid."}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health():
|
||||
model_exists = os.path.exists(os.path.join(MODELS_DIR, 'face_model.pkl'))
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"model_loaded": model_exists,
|
||||
"datasets_dir": DATASETS_DIR,
|
||||
"models_dir": MODELS_DIR,
|
||||
})
|
||||
|
||||
|
||||
@app.route('/extract-frames', methods=['POST'])
|
||||
@require_api_key
|
||||
def api_extract_frames():
|
||||
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)
|
||||
|
||||
logger.info(
|
||||
f"Extract frames berhasil untuk user {user_id}. "
|
||||
f"Frames: {result['total_extracted']}"
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"message": f"Berhasil mengekstrak {result['total_extracted']} frame wajah frontal",
|
||||
**result
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Extract frames 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('/train-model', methods=['POST'])
|
||||
@require_api_key
|
||||
def api_train_model():
|
||||
data = request.get_json(silent=True) or {}
|
||||
approved_user_ids = data.get('approved_user_ids')
|
||||
|
||||
if not approved_user_ids or not isinstance(approved_user_ids, list):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "approved_user_ids wajib diisi sebagai array."
|
||||
}), 400
|
||||
|
||||
approved_str = [str(uid) for uid in approved_user_ids]
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
f"Memulai training model SVM. "
|
||||
f"Approved users: {approved_str}"
|
||||
)
|
||||
|
||||
result = train_model(DATASETS_DIR, MODELS_DIR, approved_str)
|
||||
|
||||
logger.info(
|
||||
f"Training selesai. Users: {result['total_users']}, "
|
||||
f"CV: {result['cv_score']*100:.2f}%, "
|
||||
f"Test: {result['test_accuracy']*100:.2f}%"
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"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
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Training model GAGAL: {str(e)}")
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/verify-face', methods=['POST'])
|
||||
@require_api_key
|
||||
def api_verify_face():
|
||||
if 'file' not in request.files:
|
||||
return jsonify({"status": "error", "message": "File tidak ditemukan."}), 400
|
||||
|
||||
user_id = request.form.get('user_id')
|
||||
is_video = request.form.get('is_video', 'false').lower() == 'true'
|
||||
|
||||
if not user_id:
|
||||
return jsonify({"status": "error", "message": "user_id wajib diisi."}), 400
|
||||
|
||||
uploaded_file = request.files['file']
|
||||
ext = 'mp4' if is_video else 'jpg'
|
||||
temp_file = os.path.join(TEMP_DIR, f"verify_{user_id}_{uuid.uuid4().hex[:8]}.{ext}")
|
||||
|
||||
try:
|
||||
uploaded_file.save(temp_file)
|
||||
|
||||
result = verify_face(MODELS_DIR, user_id, temp_file, is_video=is_video)
|
||||
|
||||
logger.info(
|
||||
f"Verifikasi user {user_id}: "
|
||||
f"status={result.get('verification_status')}, "
|
||||
f"match={result.get('match')}, "
|
||||
f"svm_df={result.get('svm_df')}, "
|
||||
f"confidence={result.get('confidence')}, "
|
||||
f"predicted={result.get('predicted_user')}, "
|
||||
f"blur={result.get('blur_score')}, "
|
||||
f"frames_approved={result.get('frames_approved')}/{result.get('frames_total')}"
|
||||
)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Verifikasi GAGAL untuk user {user_id}: {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")
|
||||
logger.info(f"Datasets: {DATASETS_DIR}")
|
||||
logger.info(f"Models: {MODELS_DIR}")
|
||||
logger.info("=" * 50)
|
||||
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
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')
|
||||
MODELS_DIR = os.path.join(STORAGE_DIR, 'face_models')
|
||||
TEMP_DIR = os.path.join(STORAGE_DIR, 'temp')
|
||||
|
||||
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
|
||||
|
||||
API_KEY = os.environ.get('FLASK_ML_API_KEY', 'dev-api-key-mpg-hris')
|
||||
|
||||
for d in [STORAGE_DIR, DATASETS_DIR, MODELS_DIR, TEMP_DIR]:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
flask
|
||||
flask-cors
|
||||
opencv-contrib-python-headless
|
||||
numpy
|
||||
scikit-learn
|
||||
scikit-image
|
||||
joblib
|
||||
Pillow
|
||||
gunicorn
|
||||
|
|
@ -1 +0,0 @@
|
|||
# Services package
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import cv2
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from utils.lbp_features import preprocess_face, FACE_SIZE
|
||||
|
||||
BLUR_THRESHOLD = 30.0
|
||||
MIN_FACE_RATIO = 0.15
|
||||
|
||||
face_cascade = cv2.CascadeClassifier(
|
||||
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
|
||||
)
|
||||
|
||||
|
||||
def get_blur_score(img):
|
||||
return cv2.Laplacian(img, cv2.CV_64F).var()
|
||||
|
||||
|
||||
def detect_frontal_face(gray_frame):
|
||||
h, w = gray_frame.shape
|
||||
min_size = int(min(h, w) * MIN_FACE_RATIO)
|
||||
|
||||
faces = face_cascade.detectMultiScale(
|
||||
gray_frame, scaleFactor=1.1, minNeighbors=5,
|
||||
minSize=(min_size, min_size)
|
||||
)
|
||||
|
||||
if len(faces) == 0:
|
||||
faces = face_cascade.detectMultiScale(
|
||||
gray_frame, scaleFactor=1.05, minNeighbors=4,
|
||||
minSize=(min_size, min_size)
|
||||
)
|
||||
|
||||
if len(faces) == 0:
|
||||
return None
|
||||
|
||||
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
|
||||
return faces[0]
|
||||
|
||||
|
||||
def estimate_yaw_from_symmetry(gray_face):
|
||||
h, w = gray_face.shape
|
||||
mid = w // 2
|
||||
|
||||
left_half = gray_face[:, :mid].astype(np.float64)
|
||||
right_half = cv2.flip(gray_face[:, mid:], 1).astype(np.float64)
|
||||
|
||||
min_w = min(left_half.shape[1], right_half.shape[1])
|
||||
left_half = left_half[:, :min_w]
|
||||
right_half = right_half[:, :min_w]
|
||||
|
||||
if left_half.size == 0 or right_half.size == 0:
|
||||
return 999.0
|
||||
|
||||
left_mean = np.mean(left_half)
|
||||
right_mean = np.mean(right_half)
|
||||
|
||||
diff = abs(left_mean - right_mean)
|
||||
estimated_yaw = diff * 0.8
|
||||
|
||||
return estimated_yaw
|
||||
|
||||
|
||||
def process_frame(frame, gray_frame):
|
||||
face_rect = detect_frontal_face(gray_frame)
|
||||
if face_rect is None:
|
||||
return None, None, "no_face", 0.0, 0.0
|
||||
|
||||
(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)
|
||||
|
||||
face_crop_gray = gray_frame[y1:y2, x1:x2]
|
||||
face_crop_color = frame[y1:y2, x1:x2]
|
||||
|
||||
face_resized = cv2.resize(face_crop_gray, FACE_SIZE, interpolation=cv2.INTER_AREA)
|
||||
|
||||
blur_score = get_blur_score(face_resized)
|
||||
if blur_score < BLUR_THRESHOLD:
|
||||
return None, None, f"blur ({round(blur_score, 1)})", blur_score, 0.0
|
||||
|
||||
yaw_estimate = estimate_yaw_from_symmetry(face_resized)
|
||||
|
||||
return face_resized, face_crop_color, "ok", blur_score, yaw_estimate
|
||||
|
||||
|
||||
def extract_frames(video_path, output_dir, target_frames=30):
|
||||
if not os.path.exists(video_path):
|
||||
raise Exception(f"Video tidak ditemukan: {video_path}")
|
||||
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
else:
|
||||
for f in os.listdir(output_dir):
|
||||
if (f.startswith("frame_") or f.startswith("raw_frame_")) and f.endswith(".jpg"):
|
||||
os.remove(os.path.join(output_dir, f))
|
||||
|
||||
cap = cv2.VideoCapture(video_path)
|
||||
if not cap.isOpened():
|
||||
raise Exception("Gagal membuka file video.")
|
||||
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
if total_frames <= 0:
|
||||
raise Exception("Video tidak valid (0 frame).")
|
||||
|
||||
candidates = []
|
||||
frame_idx = 0
|
||||
skipped_no_face = 0
|
||||
skipped_blur = 0
|
||||
skipped_side = 0
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
face_img, face_color, status, blur_score, yaw = process_frame(frame, gray)
|
||||
|
||||
if face_img is None:
|
||||
if "blur" in status:
|
||||
skipped_blur += 1
|
||||
else:
|
||||
skipped_no_face += 1
|
||||
frame_idx += 1
|
||||
continue
|
||||
|
||||
candidates.append({
|
||||
'frame_idx': frame_idx,
|
||||
'face_gray': face_img,
|
||||
'face_color': face_color,
|
||||
'blur_score': blur_score,
|
||||
'yaw': yaw
|
||||
})
|
||||
|
||||
frame_idx += 1
|
||||
|
||||
cap.release()
|
||||
|
||||
if len(candidates) == 0:
|
||||
raise Exception(
|
||||
"Tidak ada frame wajah frontal berkualitas yang berhasil diekstrak. "
|
||||
"Pastikan wajah menghadap depan dengan pencahayaan cukup."
|
||||
)
|
||||
|
||||
candidates.sort(key=lambda c: c['blur_score'], reverse=True)
|
||||
|
||||
if len(candidates) > target_frames:
|
||||
candidates_by_idx = sorted(candidates, key=lambda c: c['frame_idx'])
|
||||
|
||||
chunk_size = len(candidates_by_idx) // target_frames
|
||||
selected = []
|
||||
|
||||
for i in range(target_frames):
|
||||
start = i * chunk_size
|
||||
end = min(start + chunk_size, len(candidates_by_idx))
|
||||
chunk = candidates_by_idx[start:end]
|
||||
|
||||
best_in_chunk = max(chunk, key=lambda c: c['blur_score'])
|
||||
selected.append(best_in_chunk)
|
||||
|
||||
candidates = sorted(selected, key=lambda c: c['frame_idx'])
|
||||
|
||||
saved_count = 0
|
||||
for cand in candidates:
|
||||
filename = f"frame_{saved_count:03d}.jpg"
|
||||
filename_raw = f"raw_frame_{saved_count:03d}.jpg"
|
||||
cv2.imwrite(
|
||||
os.path.join(output_dir, filename),
|
||||
cand['face_gray'],
|
||||
[int(cv2.IMWRITE_JPEG_QUALITY), 95]
|
||||
)
|
||||
cv2.imwrite(os.path.join(output_dir, filename_raw), cand['face_color'])
|
||||
saved_count += 1
|
||||
|
||||
return {
|
||||
"total_video_frames": total_frames,
|
||||
"video_fps": round(fps, 1),
|
||||
"total_candidates": len(candidates),
|
||||
"total_extracted": saved_count,
|
||||
"target_frames": target_frames,
|
||||
"skipped_no_face": skipped_no_face,
|
||||
"skipped_blur": skipped_blur,
|
||||
"skipped_side_face": skipped_side,
|
||||
"output_dir": output_dir
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import cv2
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import numpy as np
|
||||
import joblib
|
||||
from sklearn.svm import SVC
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.pipeline import Pipeline
|
||||
from sklearn.model_selection import train_test_split, GridSearchCV
|
||||
from skimage.feature import local_binary_pattern
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
from utils.lbp_features import (
|
||||
extract_lbp_features, extract_lbp_from_augmented,
|
||||
augmentasi_lbp, preprocess_face, FACE_SIZE, LBP_SCALES
|
||||
)
|
||||
|
||||
|
||||
def load_images(dataset_path):
|
||||
valid_ext = ('.jpg', '.jpeg', '.png')
|
||||
images = []
|
||||
|
||||
if not os.path.exists(dataset_path):
|
||||
return images
|
||||
|
||||
files = sorted([
|
||||
f for f in os.listdir(dataset_path)
|
||||
if f.lower().endswith(valid_ext) and (
|
||||
f.startswith('frame_') or f.startswith('selfie_')
|
||||
)
|
||||
])
|
||||
|
||||
for filename in files:
|
||||
filepath = os.path.join(dataset_path, filename)
|
||||
img = cv2.imread(filepath, cv2.IMREAD_GRAYSCALE)
|
||||
if img is not None:
|
||||
if img.shape != FACE_SIZE:
|
||||
img = cv2.resize(img, FACE_SIZE, interpolation=cv2.INTER_AREA)
|
||||
images.append(img)
|
||||
|
||||
return images
|
||||
|
||||
|
||||
def train_model(base_datasets_path, model_output_path, approved_user_ids=None):
|
||||
if not os.path.exists(base_datasets_path):
|
||||
raise Exception(f"Base datasets path tidak ditemukan: {base_datasets_path}")
|
||||
|
||||
user_images_dict = {}
|
||||
label_map = {}
|
||||
user_stats = {}
|
||||
|
||||
for folder_name in sorted(os.listdir(base_datasets_path)):
|
||||
folder_path = os.path.join(base_datasets_path, folder_name)
|
||||
if not os.path.isdir(folder_path):
|
||||
continue
|
||||
|
||||
user_id = folder_name
|
||||
|
||||
if approved_user_ids and user_id not in approved_user_ids:
|
||||
continue
|
||||
|
||||
images = load_images(folder_path)
|
||||
if len(images) < 10:
|
||||
continue
|
||||
|
||||
label_map[user_id] = user_id
|
||||
user_images_dict[user_id] = images
|
||||
|
||||
if len(label_map) == 0:
|
||||
raise Exception("Tidak ada user dengan dataset valid (minimal 10 gambar per user).")
|
||||
|
||||
if len(label_map) < 2:
|
||||
raise Exception(
|
||||
"Minimal 2 user yang sudah di-approve diperlukan untuk melatih model SVM. "
|
||||
f"Saat ini baru {len(label_map)} user."
|
||||
)
|
||||
|
||||
X_ori = []
|
||||
y_ori = []
|
||||
X_aug = []
|
||||
y_aug = []
|
||||
|
||||
for user_id, images in user_images_dict.items():
|
||||
ori_count = 0
|
||||
aug_count = 0
|
||||
|
||||
for img in images:
|
||||
proc_img = preprocess_face(img)
|
||||
features = extract_lbp_features(proc_img)
|
||||
X_ori.append(features)
|
||||
y_ori.append(user_id)
|
||||
ori_count += 1
|
||||
|
||||
pola = local_binary_pattern(proc_img, P=8, R=1, method='uniform')
|
||||
lbp_uint8 = (pola * 255.0 / 9).astype(np.uint8)
|
||||
variasi = augmentasi_lbp(lbp_uint8)
|
||||
for aug_img in variasi:
|
||||
aug_features = extract_lbp_from_augmented(aug_img)
|
||||
X_aug.append(aug_features)
|
||||
y_aug.append(user_id)
|
||||
aug_count += 1
|
||||
|
||||
user_stats[user_id] = ori_count + aug_count
|
||||
|
||||
X = np.array(X_ori + X_aug)
|
||||
y = np.array(y_ori + y_aug)
|
||||
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.2, random_state=42, stratify=y
|
||||
)
|
||||
|
||||
param_grid = {
|
||||
'svm__C': [0.1, 1, 10, 20],
|
||||
'svm__gamma': ['scale', 'auto', 0.01, 0.001, 0.1]
|
||||
}
|
||||
|
||||
pipe_search = Pipeline([
|
||||
('scaler', StandardScaler()),
|
||||
('svm', SVC(kernel='rbf', probability=False))
|
||||
])
|
||||
|
||||
grid = GridSearchCV(
|
||||
pipe_search, param_grid,
|
||||
cv=5, scoring='accuracy',
|
||||
refit=False, return_train_score=True, verbose=0
|
||||
)
|
||||
grid.fit(X_train, y_train)
|
||||
|
||||
best_C = grid.best_params_['svm__C']
|
||||
best_gamma = grid.best_params_['svm__gamma']
|
||||
cv_score = grid.best_score_
|
||||
|
||||
model_final = Pipeline([
|
||||
('scaler', StandardScaler()),
|
||||
('svm', SVC(kernel='rbf', C=best_C, gamma=best_gamma, probability=True))
|
||||
])
|
||||
model_final.fit(X_train, y_train)
|
||||
test_acc = model_final.score(X_test, y_test)
|
||||
|
||||
if not os.path.exists(model_output_path):
|
||||
os.makedirs(model_output_path)
|
||||
|
||||
model_file = os.path.join(model_output_path, "face_model.pkl")
|
||||
scaler_file = os.path.join(model_output_path, "face_scaler.pkl")
|
||||
labels_file = os.path.join(model_output_path, "face_labels.json")
|
||||
|
||||
temp_model_file = os.path.join(model_output_path, "face_model_temp.pkl")
|
||||
temp_scaler_file = os.path.join(model_output_path, "face_scaler_temp.pkl")
|
||||
temp_labels_file = os.path.join(model_output_path, "face_labels_temp.json")
|
||||
|
||||
joblib.dump(model_final.named_steps['svm'], temp_model_file)
|
||||
joblib.dump(model_final.named_steps['scaler'], temp_scaler_file)
|
||||
|
||||
labels_data = {
|
||||
"classes": list(model_final.named_steps['svm'].classes_),
|
||||
"user_ids": list(label_map.keys()),
|
||||
"best_C": best_C,
|
||||
"best_gamma": str(best_gamma),
|
||||
"cv_score": round(cv_score, 4),
|
||||
"test_accuracy": round(test_acc, 4),
|
||||
}
|
||||
with open(temp_labels_file, 'w') as f:
|
||||
json.dump(labels_data, f, indent=2)
|
||||
|
||||
os.replace(temp_model_file, model_file)
|
||||
os.replace(temp_scaler_file, scaler_file)
|
||||
os.replace(temp_labels_file, labels_file)
|
||||
|
||||
return {
|
||||
"total_users": len(label_map),
|
||||
"user_stats": user_stats,
|
||||
"total_samples": len(X),
|
||||
"train_samples": len(X_train),
|
||||
"test_samples": len(X_test),
|
||||
"feature_dimension": int(X.shape[1]),
|
||||
"best_C": best_C,
|
||||
"best_gamma": str(best_gamma),
|
||||
"cv_score": round(cv_score, 4),
|
||||
"test_accuracy": round(test_acc, 4),
|
||||
"classes": list(model_final.named_steps['svm'].classes_),
|
||||
"model_path": model_file,
|
||||
"scaler_path": scaler_file,
|
||||
"labels_path": labels_file
|
||||
}
|
||||
|
|
@ -1,366 +0,0 @@
|
|||
import cv2
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import numpy as np
|
||||
import joblib
|
||||
|
||||
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
|
||||
|
||||
UNKNOWN_LABEL = "unknown"
|
||||
BLUR_THRESHOLD = 30.0
|
||||
DF_APPROVED = 1.5
|
||||
DF_PENDING = 0.5
|
||||
|
||||
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 fix_exif_rotation(img_path):
|
||||
try:
|
||||
from PIL import Image
|
||||
pil_img = Image.open(img_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)
|
||||
return img_array
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
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 preprocess_image(image_path):
|
||||
if not os.path.exists(image_path):
|
||||
raise Exception("File gambar tidak ditemukan.")
|
||||
|
||||
img = fix_exif_rotation(image_path)
|
||||
if img is None:
|
||||
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."
|
||||
)
|
||||
|
||||
face_preprocessed = preprocess_face(face)
|
||||
return face_preprocessed, blur_score
|
||||
|
||||
|
||||
def extract_frames_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
|
||||
|
||||
face_preprocessed = preprocess_face(face_raw)
|
||||
candidates.append({
|
||||
'face': face_preprocessed,
|
||||
'blur_score': blur_score,
|
||||
'frame_idx': frame_idx,
|
||||
})
|
||||
|
||||
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]
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
def _classify_frame(features, svm, scaler, expected_user):
|
||||
classes = [str(c) for c in svm.classes_]
|
||||
features_s = scaler.transform([features])
|
||||
df_values = svm.decision_function(features_s)[0]
|
||||
proba = svm.predict_proba(features_s)[0]
|
||||
|
||||
if expected_user in classes:
|
||||
idx = classes.index(expected_user)
|
||||
user_df = float(df_values[idx] if hasattr(df_values, '__len__') else df_values)
|
||||
user_conf = float(proba[idx])
|
||||
else:
|
||||
user_df = -999.0
|
||||
user_conf = 0.0
|
||||
|
||||
df_sorted = sorted(enumerate(df_values), key=lambda x: -x[1])
|
||||
predicted_class = str(classes[df_sorted[0][0]])
|
||||
|
||||
top_df = float(df_values[df_sorted[0][0]])
|
||||
second_df = float(df_values[df_sorted[1][0]]) if len(df_sorted) > 1 else -999.0
|
||||
df_margin = top_df - second_df
|
||||
|
||||
# === Verifikasi 1:1 ===
|
||||
# Untuk model LBP+SVM, cukup cek skor OVR user sendiri.
|
||||
# Gap check dihapus karena terlalu ketat untuk variasi pencahayaan/sudut.
|
||||
gap = top_df - user_df if predicted_class != expected_user else 0.0
|
||||
|
||||
if user_df >= DF_APPROVED:
|
||||
status = "APPROVED"
|
||||
is_match = True
|
||||
elif user_df >= DF_PENDING:
|
||||
status = "PENDING"
|
||||
is_match = True
|
||||
else:
|
||||
status = "REJECTED"
|
||||
is_match = False
|
||||
|
||||
return {
|
||||
'is_match': is_match,
|
||||
'status': status,
|
||||
'user_df': user_df,
|
||||
'confidence': user_conf,
|
||||
'predicted_class': predicted_class,
|
||||
'df_margin': round(df_margin, 4),
|
||||
'gap': round(gap, 4),
|
||||
}
|
||||
|
||||
|
||||
def _aggregate_frame_results(frame_results):
|
||||
total = len(frame_results)
|
||||
approved = sum(1 for r in frame_results if r['status'] == 'APPROVED')
|
||||
pending = sum(1 for r in frame_results if r['status'] == 'PENDING')
|
||||
rejected = sum(1 for r in frame_results if r['status'] == 'REJECTED')
|
||||
|
||||
approved_ratio = approved / total
|
||||
pending_ratio = pending / total
|
||||
|
||||
avg_df = float(np.mean([r['user_df'] for r in frame_results]))
|
||||
avg_conf = float(np.mean([r['confidence'] for r in frame_results]))
|
||||
|
||||
if approved_ratio >= 0.4:
|
||||
final_status = "APPROVED"
|
||||
final_match = True
|
||||
elif (approved_ratio + pending_ratio) >= 0.5:
|
||||
final_status = "PENDING"
|
||||
final_match = True
|
||||
else:
|
||||
final_status = "REJECTED"
|
||||
final_match = False
|
||||
|
||||
return {
|
||||
'verification_status': final_status,
|
||||
'match': final_match,
|
||||
'avg_df': round(avg_df, 4),
|
||||
'confidence': round(avg_conf, 4),
|
||||
'frames_total': total,
|
||||
'frames_approved': approved,
|
||||
'frames_pending': pending,
|
||||
'frames_rejected': rejected,
|
||||
'approved_ratio': round(approved_ratio, 3),
|
||||
}
|
||||
|
||||
|
||||
def verify_face(model_dir, user_id, input_path, is_video=False):
|
||||
model_file = os.path.join(model_dir, "face_model.pkl")
|
||||
scaler_file = os.path.join(model_dir, "face_scaler.pkl")
|
||||
labels_file = os.path.join(model_dir, "face_labels.json")
|
||||
|
||||
if not os.path.exists(model_file):
|
||||
raise Exception("Model wajah belum tersedia. Belum ada data training.")
|
||||
if not os.path.exists(scaler_file):
|
||||
raise Exception("File scaler belum tersedia.")
|
||||
if not os.path.exists(labels_file):
|
||||
raise Exception("File label belum tersedia.")
|
||||
|
||||
svm = joblib.load(model_file)
|
||||
scaler = joblib.load(scaler_file)
|
||||
|
||||
expected_user = str(user_id)
|
||||
|
||||
if is_video:
|
||||
try:
|
||||
frames = extract_frames_from_video(input_path, target_frames=10)
|
||||
except Exception as e_vid:
|
||||
return {
|
||||
"status": "success",
|
||||
"match": False,
|
||||
"confidence": 0,
|
||||
"verification_status": "PREPROCESSING_FAILED",
|
||||
"message": str(e_vid),
|
||||
"blur_score": 0,
|
||||
"frames_total": 0,
|
||||
}
|
||||
|
||||
frame_results = []
|
||||
blur_scores = []
|
||||
|
||||
for i, frame_data in enumerate(frames):
|
||||
face = frame_data['face']
|
||||
blur_scores.append(frame_data['blur_score'])
|
||||
features = extract_lbp_features(face)
|
||||
result = _classify_frame(features, svm, scaler, expected_user)
|
||||
frame_results.append(result)
|
||||
logger.info(
|
||||
f" Frame {i}: predicted={result['predicted_class']}, "
|
||||
f"user_df={result['user_df']:.4f}, "
|
||||
f"gap={result['gap']:.4f}, "
|
||||
f"conf={result['confidence']:.4f}, "
|
||||
f"status={result['status']}"
|
||||
)
|
||||
|
||||
aggregated = _aggregate_frame_results(frame_results)
|
||||
avg_blur = float(np.mean(blur_scores))
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"match": bool(aggregated['match']),
|
||||
"verification_status": aggregated['verification_status'],
|
||||
"confidence": aggregated['confidence'],
|
||||
"svm_df": aggregated['avg_df'],
|
||||
"blur_score": round(avg_blur, 1),
|
||||
"frames_total": aggregated['frames_total'],
|
||||
"frames_approved": aggregated['frames_approved'],
|
||||
"frames_pending": aggregated['frames_pending'],
|
||||
"frames_rejected": aggregated['frames_rejected'],
|
||||
"approved_ratio": aggregated['approved_ratio'],
|
||||
"predicted_user": expected_user if aggregated['match'] else "unknown",
|
||||
"actual_predicted": max(set(r['predicted_class'] for r in frame_results), key=lambda c: sum(1 for r in frame_results if r['predicted_class'] == c)),
|
||||
"expected_user": expected_user,
|
||||
"user_id": int(user_id) if str(user_id).isdigit() else str(user_id),
|
||||
"message": None,
|
||||
}
|
||||
|
||||
else:
|
||||
try:
|
||||
processed_face, blur_score = preprocess_image(input_path)
|
||||
except Exception as e_proc:
|
||||
return {
|
||||
"status": "success",
|
||||
"match": False,
|
||||
"confidence": 0,
|
||||
"verification_status": "PREPROCESSING_FAILED",
|
||||
"message": str(e_proc),
|
||||
"blur_score": 0,
|
||||
}
|
||||
|
||||
features = extract_lbp_features(processed_face)
|
||||
result = _classify_frame(features, svm, scaler, expected_user)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"match": bool(result['is_match']),
|
||||
"verification_status": result['status'],
|
||||
"confidence": float(round(result['confidence'], 4)),
|
||||
"svm_df": float(round(result['user_df'], 4)),
|
||||
"predicted_user": result['predicted_class'],
|
||||
"expected_user": expected_user,
|
||||
"blur_score": float(round(blur_score, 1)),
|
||||
"user_id": int(user_id) if str(user_id).isdigit() else str(user_id),
|
||||
"message": None,
|
||||
}
|
||||
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |