firgiawann commited on
Commit
f7a2768
·
verified ·
1 Parent(s): b8dda21

Deploy YOLOv8 attendance validator app

Browse files
Files changed (4) hide show
  1. .gitignore +36 -0
  2. README.md +84 -6
  3. app.py +296 -0
  4. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.so
7
+ .Python
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+
11
+ # Virtual environment
12
+ .venv/
13
+ venv/
14
+ env/
15
+
16
+ # Gradio / local app cache
17
+ .gradio/
18
+
19
+ # Build artifacts
20
+ build/
21
+ dist/
22
+ *.egg-info/
23
+
24
+ # Notebook / misc
25
+ .ipynb_checkpoints/
26
+
27
+ # OS/editor
28
+ .DS_Store
29
+ Thumbs.db
30
+ .vscode/
31
+
32
+ # Optional model/log artifacts
33
+ *.pt
34
+ *.pth
35
+ !best.pt
36
+ logs/
README.md CHANGED
@@ -1,12 +1,90 @@
1
  ---
2
- title: KLP2
3
- emoji: 🏃
4
- colorFrom: red
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.12.0
8
  app_file: app.py
9
- pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Validasi Kehadiran Mahasiswa YOLOv8
3
+ emoji: 🎓
4
+ colorFrom: blue
5
  colorTo: green
6
  sdk: gradio
7
+ python_version: 3.10
8
  app_file: app.py
 
9
  ---
10
 
11
+ # Implementasi Klasifikasi Kehadiran Mahasiswa Berbasis YOLOv8
12
+
13
+ Proyek ini menyiapkan demo riset untuk validasi kehadiran mahasiswa di dalam ruang kelas menggunakan deteksi objek YOLOv8 dan OpenCV.
14
+
15
+ ## Tujuan Proyek
16
+
17
+ Membangun aplikasi yang dapat:
18
+ - menerima input gambar kelas (upload atau webcam),
19
+ - mendeteksi objek kelas `person`,
20
+ - menghitung jumlah mahasiswa terdeteksi,
21
+ - membandingkan hasil deteksi dengan jumlah presensi,
22
+ - menampilkan status validasi:
23
+ - `Sesuai` jika jumlah deteksi sama dengan presensi,
24
+ - `Tidak sesuai` jika berbeda.
25
+
26
+ ## Cara Kerja Validasi Presensi
27
+
28
+ 1. Model YOLOv8 dimuat dari `best.pt` (lokal), atau dari Hugging Face Hub (opsional).
29
+ 2. Gambar diproses untuk deteksi objek.
30
+ 3. Sistem menghitung jumlah objek `person`.
31
+ 4. Jumlah deteksi dibandingkan dengan jumlah presensi dari pengguna.
32
+ 5. Aplikasi menampilkan:
33
+ - gambar teranotasi bounding box,
34
+ - ringkasan jumlah terdeteksi,
35
+ - jumlah presensi,
36
+ - status validasi.
37
+
38
+ ## Evaluasi Manual (Confusion Matrix)
39
+
40
+ Aplikasi menyediakan panel evaluasi manual:
41
+ - setelah inferensi, pilih label aktual (`Sesuai` / `Tidak sesuai`),
42
+ - klik **Update Confusion Matrix**,
43
+ - panel akan menampilkan nilai `TP`, `FP`, `FN`, `TN`,
44
+ - metrik `Accuracy`, `Precision`, `Recall`, dan `F1-Score` dihitung langsung dengan rumus.
45
+
46
+ ## Struktur File yang Diharapkan
47
+
48
+ - `app.py` - aplikasi Gradio untuk Spaces
49
+ - `requirements.txt` - dependensi Python
50
+ - `.gitignore` - aturan ignore
51
+ - `best.pt` - model YOLOv8 hasil training (disarankan diletakkan di root project)
52
+
53
+ Opsional environment variable:
54
+ - `MODEL_PATH` : path model lokal custom
55
+ - `HF_MODEL_REPO` : repo model Hugging Face Hub (misalnya `username/repo-model`)
56
+ - `HF_MODEL_FILE` : nama file model di repo Hub (default `best.pt`)
57
+
58
+ ## Menjalankan Secara Lokal
59
+
60
+ ```bash
61
+ python -m venv .venv
62
+ # Windows
63
+ .venv\Scripts\activate
64
+ # Linux/macOS
65
+ # source .venv/bin/activate
66
+
67
+ pip install -r requirements.txt
68
+ python app.py
69
+ ```
70
+
71
+ Buka URL lokal yang muncul di terminal (default Gradio).
72
+
73
+ ## Deploy ke Hugging Face Spaces
74
+
75
+ 1. Buat Space baru dengan SDK **Gradio**.
76
+ 2. Upload file:
77
+ - `app.py`
78
+ - `requirements.txt`
79
+ - `README.md`
80
+ - `.gitignore`
81
+ - `best.pt` (atau gunakan env Hub fallback)
82
+ 3. Jika model dari Hub, atur Secrets/Variables:
83
+ - `HF_MODEL_REPO`
84
+ - `HF_MODEL_FILE` (opsional)
85
+ 4. Space akan build otomatis dan menjalankan `app.py`.
86
+
87
+ ## Catatan
88
+
89
+ - Fitur utama saat ini fokus pada inferensi gambar.
90
+ - Kerangka ekstensi video sudah disiapkan sebagai stub, tetapi pipeline video penuh belum diimplementasikan.
app.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from functools import lru_cache
3
+ from typing import Any, Dict, Tuple
4
+
5
+ import cv2
6
+ import gradio as gr
7
+ import numpy as np
8
+ from huggingface_hub import hf_hub_download
9
+ from ultralytics import YOLO
10
+
11
+
12
+ CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
13
+
14
+
15
+ def resolve_model_path() -> str:
16
+ """Resolve YOLO model path from local file or Hugging Face Hub."""
17
+ model_env = os.getenv("MODEL_PATH", "").strip()
18
+ candidate_path = model_env or os.path.join(os.path.dirname(__file__), "best.pt")
19
+
20
+ if os.path.exists(candidate_path):
21
+ return candidate_path
22
+
23
+ hf_repo = os.getenv("HF_MODEL_REPO", "").strip()
24
+ hf_file = os.getenv("HF_MODEL_FILE", "best.pt").strip()
25
+ if hf_repo:
26
+ return hf_hub_download(repo_id=hf_repo, filename=hf_file)
27
+
28
+ raise FileNotFoundError(
29
+ "Model tidak ditemukan. Simpan file 'best.pt' di root project "
30
+ "atau set environment MODEL_PATH. "
31
+ "Opsional fallback Hugging Face Hub: set HF_MODEL_REPO dan HF_MODEL_FILE."
32
+ )
33
+
34
+
35
+ @lru_cache(maxsize=1)
36
+ def load_model() -> YOLO:
37
+ model_path = resolve_model_path()
38
+ return YOLO(model_path)
39
+
40
+
41
+ def format_summary(detected_count: int, attendance_count: int, status: str) -> str:
42
+ return (
43
+ f"Jumlah terdeteksi: {detected_count}\n"
44
+ f"Jumlah presensi: {attendance_count}\n"
45
+ f"Status validasi: {status}"
46
+ )
47
+
48
+
49
+ def _safe_div(num: float, den: float) -> float:
50
+ return 0.0 if den == 0 else num / den
51
+
52
+
53
+ def compute_metrics(tp: int, fp: int, fn: int, tn: int) -> Dict[str, float]:
54
+ total = tp + fp + fn + tn
55
+ accuracy = _safe_div(tp + tn, total)
56
+ precision = _safe_div(tp, tp + fp)
57
+ recall = _safe_div(tp, tp + fn)
58
+ f1 = _safe_div(2 * precision * recall, precision + recall)
59
+ return {
60
+ "accuracy": accuracy,
61
+ "precision": precision,
62
+ "recall": recall,
63
+ "f1": f1,
64
+ }
65
+
66
+
67
+ def render_evaluation_panel(cm_state: Dict[str, int]) -> str:
68
+ tp = int(cm_state.get("tp", 0))
69
+ fp = int(cm_state.get("fp", 0))
70
+ fn = int(cm_state.get("fn", 0))
71
+ tn = int(cm_state.get("tn", 0))
72
+ m = compute_metrics(tp, fp, fn, tn)
73
+
74
+ return f"""
75
+ <div class="eval-grid">
76
+ <div class="card">
77
+ <h3>Confusion Matrix</h3>
78
+ <table class="cm-table">
79
+ <thead>
80
+ <tr>
81
+ <th></th>
82
+ <th>Prediksi Sesuai</th>
83
+ <th>Prediksi Tidak sesuai</th>
84
+ </tr>
85
+ </thead>
86
+ <tbody>
87
+ <tr>
88
+ <th>Aktual Sesuai</th>
89
+ <td>TP = {tp}</td>
90
+ <td>FN = {fn}</td>
91
+ </tr>
92
+ <tr>
93
+ <th>Aktual Tidak sesuai</th>
94
+ <td>FP = {fp}</td>
95
+ <td>TN = {tn}</td>
96
+ </tr>
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ <div class="card">
101
+ <h3>Perhitungan Rumus</h3>
102
+ <p><b>Accuracy</b> = (TP + TN) / (TP + FP + FN + TN)<br>
103
+ = ({tp} + {tn}) / ({tp + fp + fn + tn}) = <b>{m["accuracy"]:.4f}</b></p>
104
+ <p><b>Precision</b> = TP / (TP + FP)<br>
105
+ = {tp} / ({tp + fp}) = <b>{m["precision"]:.4f}</b></p>
106
+ <p><b>Recall</b> = TP / (TP + FN)<br>
107
+ = {tp} / ({tp + fn}) = <b>{m["recall"]:.4f}</b></p>
108
+ <p><b>F1-Score</b> = 2 * (Precision * Recall) / (Precision + Recall)<br>
109
+ = <b>{m["f1"]:.4f}</b></p>
110
+ </div>
111
+ </div>
112
+ """
113
+
114
+
115
+ def _resolve_person_class_id(result: Any) -> int:
116
+ names = getattr(result, "names", {})
117
+ if isinstance(names, dict):
118
+ for idx, name in names.items():
119
+ if str(name).lower() == "person":
120
+ return int(idx)
121
+ if isinstance(names, list):
122
+ for idx, name in enumerate(names):
123
+ if str(name).lower() == "person":
124
+ return idx
125
+ return 0
126
+
127
+
128
+ def run_attendance_validation(
129
+ image: np.ndarray, attendance_count: float, conf_threshold: float
130
+ ) -> Tuple[np.ndarray, str, str]:
131
+ if image is None:
132
+ raise ValueError("Silakan unggah gambar atau gunakan kamera terlebih dahulu.")
133
+
134
+ try:
135
+ target_attendance = int(attendance_count)
136
+ except (TypeError, ValueError):
137
+ target_attendance = 0
138
+
139
+ model = load_model()
140
+ image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
141
+ results = model.predict(source=image_bgr, conf=conf_threshold, verbose=False)
142
+ result = results[0]
143
+
144
+ person_class_id = _resolve_person_class_id(result)
145
+ classes = result.boxes.cls.cpu().numpy() if result.boxes is not None else np.array([])
146
+ detected_count = int(np.sum(classes == person_class_id))
147
+
148
+ annotated_bgr = result.plot()
149
+ annotated_rgb = cv2.cvtColor(annotated_bgr, cv2.COLOR_BGR2RGB)
150
+
151
+ status = "Sesuai" if detected_count == target_attendance else "Tidak sesuai"
152
+ summary = format_summary(detected_count, target_attendance, status)
153
+ return annotated_rgb, summary, status
154
+
155
+
156
+ def update_confusion_matrix(
157
+ cm_state: Dict[str, int], predicted_label: str, actual_label: str
158
+ ) -> Tuple[Dict[str, int], str, str]:
159
+ state = dict(cm_state or CM_INIT)
160
+
161
+ if predicted_label not in ("Sesuai", "Tidak sesuai"):
162
+ panel = render_evaluation_panel(state)
163
+ return state, panel, "Lakukan validasi gambar terlebih dahulu sebelum update confusion matrix."
164
+
165
+ if actual_label == "Sesuai" and predicted_label == "Sesuai":
166
+ state["tp"] += 1
167
+ elif actual_label == "Sesuai" and predicted_label == "Tidak sesuai":
168
+ state["fn"] += 1
169
+ elif actual_label == "Tidak sesuai" and predicted_label == "Sesuai":
170
+ state["fp"] += 1
171
+ else:
172
+ state["tn"] += 1
173
+
174
+ panel = render_evaluation_panel(state)
175
+ return state, panel, "Confusion matrix berhasil diperbarui."
176
+
177
+
178
+ def reset_confusion_matrix() -> Tuple[Dict[str, int], str, str]:
179
+ state = dict(CM_INIT)
180
+ panel = render_evaluation_panel(state)
181
+ return state, panel, "Confusion matrix direset ke nol."
182
+
183
+
184
+ def run_inference_ui(
185
+ image: np.ndarray, attendance_count: float, conf_threshold: float
186
+ ) -> Tuple[np.ndarray, str, str]:
187
+ try:
188
+ return run_attendance_validation(image, attendance_count, conf_threshold)
189
+ except Exception as err:
190
+ summary = (
191
+ "Proses gagal.\n"
192
+ f"Detail: {err}\n\n"
193
+ "Tips:\n"
194
+ "1) Pastikan file model tersedia (best.pt atau MODEL_PATH).\n"
195
+ "2) Jika menggunakan Hugging Face Hub, set HF_MODEL_REPO dan HF_MODEL_FILE."
196
+ )
197
+ return image, summary, ""
198
+
199
+
200
+ def run_video_validation_stub(video_path: str) -> None:
201
+ """
202
+ Extension point for future video attendance pipeline.
203
+ This project intentionally keeps image inference as the primary feature.
204
+ """
205
+ raise NotImplementedError(
206
+ f"Video pipeline belum diimplementasikan. Input diterima: {video_path}"
207
+ )
208
+
209
+
210
+ CUSTOM_CSS = """
211
+ .container {max-width: 1200px !important; margin: auto;}
212
+ .app-title {text-align: center; margin-bottom: 6px;}
213
+ .app-subtitle {text-align: center; color: #4a5568; margin-top: 0;}
214
+ .eval-grid {display: grid; grid-template-columns: 1fr; gap: 12px;}
215
+ .card {border: 1px solid #e2e8f0; border-radius: 10px; padding: 12px; background: #ffffff;}
216
+ .cm-table {width: 100%; border-collapse: collapse;}
217
+ .cm-table th, .cm-table td {border: 1px solid #cbd5e1; padding: 8px; text-align: center;}
218
+ @media (min-width: 980px) {.eval-grid {grid-template-columns: 1fr 1fr;}}
219
+ """
220
+
221
+
222
+ with gr.Blocks(css=CUSTOM_CSS, title="Validasi Kehadiran Mahasiswa - YOLOv8") as demo:
223
+ gr.HTML(
224
+ """
225
+ <h1 class="app-title">Validasi Kehadiran Mahasiswa Berbasis YOLOv8</h1>
226
+ <p class="app-subtitle">
227
+ Implementasi klasifikasi kehadiran dalam ruang kelas berbasis citra secara real-time (demo riset).
228
+ </p>
229
+ """
230
+ )
231
+
232
+ cm_state = gr.State(dict(CM_INIT))
233
+ predicted_label_state = gr.State("")
234
+
235
+ with gr.Row():
236
+ with gr.Column(scale=2):
237
+ image_input = gr.Image(
238
+ label="Input Gambar Kelas",
239
+ type="numpy",
240
+ sources=["upload", "webcam"],
241
+ )
242
+ attendance_input = gr.Number(
243
+ label="Jumlah Presensi",
244
+ value=0,
245
+ precision=0,
246
+ minimum=0,
247
+ )
248
+ conf_input = gr.Slider(
249
+ minimum=0.1,
250
+ maximum=1.0,
251
+ value=0.25,
252
+ step=0.05,
253
+ label="Confidence Threshold",
254
+ )
255
+ run_btn = gr.Button("Proses Validasi", variant="primary")
256
+
257
+ output_image = gr.Image(label="Hasil Deteksi (Bounding Box)", type="numpy")
258
+ output_summary = gr.Textbox(
259
+ label="Ringkasan Validasi",
260
+ lines=5,
261
+ interactive=False,
262
+ )
263
+
264
+ with gr.Column(scale=1):
265
+ gr.Markdown("### Evaluasi Manual")
266
+ actual_label_input = gr.Radio(
267
+ choices=["Sesuai", "Tidak sesuai"],
268
+ value="Sesuai",
269
+ label="Label Aktual Sampel",
270
+ )
271
+ update_cm_btn = gr.Button("Update Confusion Matrix")
272
+ reset_cm_btn = gr.Button("Reset Confusion Matrix")
273
+ eval_status = gr.Markdown("Silakan proses gambar terlebih dahulu.")
274
+ cm_panel = gr.HTML(render_evaluation_panel(CM_INIT))
275
+
276
+ run_btn.click(
277
+ fn=run_inference_ui,
278
+ inputs=[image_input, attendance_input, conf_input],
279
+ outputs=[output_image, output_summary, predicted_label_state],
280
+ )
281
+
282
+ update_cm_btn.click(
283
+ fn=update_confusion_matrix,
284
+ inputs=[cm_state, predicted_label_state, actual_label_input],
285
+ outputs=[cm_state, cm_panel, eval_status],
286
+ )
287
+
288
+ reset_cm_btn.click(
289
+ fn=reset_confusion_matrix,
290
+ inputs=[],
291
+ outputs=[cm_state, cm_panel, eval_status],
292
+ )
293
+
294
+
295
+ if __name__ == "__main__":
296
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gradio>=4.44.1
2
+ ultralytics>=8.2.0
3
+ opencv-python-headless>=4.10.0.84
4
+ huggingface_hub>=0.23.0
5
+ numpy>=1.26.0