firgiawann commited on
Commit
cc56087
·
verified ·
1 Parent(s): 9f3d5c5

Simplify to check+classification only, remove batch input and training UI

Browse files
Files changed (3) hide show
  1. README.md +41 -80
  2. app.py +35 -247
  3. requirements.txt +0 -3
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Validasi Kehadiran Mahasiswa YOLOv8
3
  emoji: 🎓
4
  colorFrom: blue
5
  colorTo: green
@@ -8,108 +8,69 @@ 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 `yolov8n.pt` (lokal default), 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 Klasifikasi (Confusion Matrix Image)
39
 
40
- Aplikasi menyediakan evaluasi batch otomatis:
41
- - upload ZIP berisi gambar evaluasi,
42
- - upload CSV ground truth (filename, presensi, actual_label),
43
- - sistem menjalankan klasifikasi untuk semua sampel,
44
- - output confusion matrix ditampilkan sebagai **gambar heatmap**,
45
- - metrik Accuracy, Precision, Recall, dan F1-Score dihitung otomatis.
46
-
47
- ## Struktur UI (Vertical Sections)
48
 
49
  - **Section 1 - Input & Proses**
50
- - upload/kamera gambar, input presensi, parameter confidence, tombol proses klasifikasi
51
- - input evaluasi batch (ZIP gambar + CSV ground truth)
 
52
  - **Section 2 - Hasil Processing**
53
- - hasil klasifikasi
54
- - grafik perbandingan (terdeteksi vs presensi)
55
- - confusion matrix image
56
- - metrik evaluasi batch
57
- - **Section 3 - Training**
58
- - upload ZIP dataset YOLO
59
- - set path `data.yaml`, epochs, image size
60
- - jalankan training dan ambil `best.pt` jika tersedia
61
 
62
- ## Struktur File yang Diharapkan
63
 
64
- - `app.py` - aplikasi Gradio untuk Spaces
65
- - `requirements.txt` - dependensi Python
 
66
  - `.gitignore` - aturan ignore
67
- - `yolov8n.pt` - model YOLOv8 baseline untuk demo awal
68
- - `best.pt` - model hasil training lanjutan (opsional, bisa dipakai kemudian)
69
 
70
  Opsional environment variable:
71
- - `MODEL_PATH` : path model lokal custom
72
- - `HF_MODEL_REPO` : repo model Hugging Face Hub (misalnya `username/repo-model`)
73
- - `HF_MODEL_FILE` : nama file model di repo Hub (default `yolov8n.pt`)
74
 
75
- ## Menjalankan Secara Lokal
76
 
77
  ```bash
78
  python -m venv .venv
79
  # Windows
80
  .venv\Scripts\activate
81
- # Linux/macOS
82
- # source .venv/bin/activate
83
 
84
  pip install -r requirements.txt
85
  python app.py
86
  ```
87
 
88
- Buka URL lokal yang muncul di terminal (default Gradio).
89
-
90
  ## Deploy ke Hugging Face Spaces
91
 
92
- 1. Buat Space baru dengan SDK **Gradio**.
93
- 2. Upload file:
94
- - `app.py`
95
- - `requirements.txt`
96
- - `README.md`
97
- - `.gitignore`
98
- - `yolov8n.pt` (atau gunakan env Hub fallback)
99
- 3. Jika model dari Hub, atur Secrets/Variables:
100
- - `HF_MODEL_REPO`
101
- - `HF_MODEL_FILE` (opsional)
102
- 4. Space akan build otomatis dan menjalankan `app.py`.
103
-
104
- ## Format CSV Evaluasi
105
-
106
- Contoh kolom yang didukung:
107
- - `filename` (atau `file`/`image`)
108
- - `presensi` (atau `attendance`)
109
- - `actual_label` (atau `label`/`ground_truth`) berisi `Sesuai` atau `Tidak sesuai`
110
-
111
- ## Catatan
112
-
113
- - Fitur utama saat ini fokus pada inferensi gambar.
114
- - Kerangka ekstensi video sudah disiapkan sebagai stub, tetapi pipeline video penuh belum diimplementasikan.
115
- - Jika nanti model training `best.pt` sudah siap, cukup set `MODEL_PATH=best.pt` atau `HF_MODEL_FILE=best.pt`.
 
1
  ---
2
+ title: Klasifikasi Kehadiran Mahasiswa YOLOv8
3
  emoji: 🎓
4
  colorFrom: blue
5
  colorTo: green
 
8
  app_file: app.py
9
  ---
10
 
11
+ # Pengecekan dan Klasifikasi Kehadiran Mahasiswa Berbasis YOLOv8
12
 
13
+ Versi ini difokuskan untuk tahap awal:
14
+ - pengecekan gambar kelas,
15
+ - deteksi objek `person`,
16
+ - klasifikasi sederhana keberadaan mahasiswa.
17
 
18
+ ## Tujuan Fitur Saat Ini
19
 
20
+ 1. Upload gambar atau ambil dari kamera.
21
+ 2. Jalankan deteksi `person` dengan YOLOv8.
22
+ 3. Tampilkan hasil:
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  - gambar teranotasi bounding box,
24
+ - jumlah person terdeteksi,
25
+ - status klasifikasi:
26
+ - `Ada Mahasiswa` jika jumlah terdeteksi > 0
27
+ - `Tidak Ada Mahasiswa` jika jumlah terdeteksi = 0
28
+ - grafik hasil pengecekan.
29
 
30
+ ## Struktur UI
 
 
 
 
 
 
 
31
 
32
  - **Section 1 - Input & Proses**
33
+ - input gambar (upload/webcam)
34
+ - confidence threshold
35
+ - tombol proses
36
  - **Section 2 - Hasil Processing**
37
+ - hasil gambar deteksi
38
+ - ringkasan klasifikasi
39
+ - grafik jumlah person terdeteksi
40
+
41
+ Catatan:
42
+ - input evaluasi batch dihapus.
43
+ - menu training dipisahkan dari halaman ini.
44
+ - input jumlah presensi belum digunakan pada versi ini.
45
 
46
+ ## File Penting
47
 
48
+ - `app.py` - aplikasi Gradio
49
+ - `requirements.txt` - dependensi
50
+ - `yolov8n.pt` - model default
51
  - `.gitignore` - aturan ignore
 
 
52
 
53
  Opsional environment variable:
54
+ - `MODEL_PATH` - path model lokal custom
55
+ - `HF_MODEL_REPO` - repo model Hugging Face Hub
56
+ - `HF_MODEL_FILE` - nama file model di Hub (default `yolov8n.pt`)
57
 
58
+ ## Menjalankan Lokal
59
 
60
  ```bash
61
  python -m venv .venv
62
  # Windows
63
  .venv\Scripts\activate
 
 
64
 
65
  pip install -r requirements.txt
66
  python app.py
67
  ```
68
 
 
 
69
  ## Deploy ke Hugging Face Spaces
70
 
71
+ Upload:
72
+ - `app.py`
73
+ - `requirements.txt`
74
+ - `README.md`
75
+ - `.gitignore`
76
+ - `yolov8n.pt` (atau gunakan env Hub fallback)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,25 +1,16 @@
1
  import io
2
  import os
3
- import tempfile
4
- import zipfile
5
- from datetime import datetime
6
  from functools import lru_cache
7
- from typing import Any, Dict, List, Tuple
8
 
9
  import cv2
10
  import gradio as gr
11
  import matplotlib.pyplot as plt
12
  import numpy as np
13
- import pandas as pd
14
- import seaborn as sns
15
  from huggingface_hub import hf_hub_download
16
- from sklearn.metrics import confusion_matrix
17
  from ultralytics import YOLO
18
 
19
 
20
- CLASS_NAMES = ["Sesuai", "Tidak sesuai"]
21
-
22
-
23
  def resolve_model_path() -> str:
24
  model_env = os.getenv("MODEL_PATH", "").strip()
25
  base_dir = os.path.dirname(__file__)
@@ -49,10 +40,6 @@ def load_model() -> YOLO:
49
  return YOLO(resolve_model_path())
50
 
51
 
52
- def _safe_div(num: float, den: float) -> float:
53
- return 0.0 if den == 0 else num / den
54
-
55
-
56
  def _resolve_person_class_id(result: Any) -> int:
57
  names = getattr(result, "names", {})
58
  if isinstance(names, dict):
@@ -79,19 +66,25 @@ def infer_image(image: np.ndarray, conf_threshold: float) -> Tuple[np.ndarray, i
79
  return annotated, detected_count
80
 
81
 
82
- def classify_status(detected_count: int, attendance_count: int) -> str:
83
- return "Sesuai" if detected_count == attendance_count else "Tidak sesuai"
84
 
85
 
86
- def draw_single_comparison_chart(detected: int, attendance: int) -> np.ndarray:
87
  fig, ax = plt.subplots(figsize=(5.0, 3.2))
88
- labels = ["Terdeteksi", "Presensi"]
89
- values = [detected, attendance]
90
- bars = ax.bar(labels, values, color=["#2b6cb0", "#2f855a"])
91
- ax.set_title("Grafik Perbandingan")
92
  ax.set_ylabel("Jumlah")
93
  for b in bars:
94
- ax.text(b.get_x() + b.get_width() / 2, b.get_height() + 0.05, str(int(b.get_height())), ha="center", va="bottom")
 
 
 
 
 
 
95
  plt.tight_layout()
96
 
97
  buf = io.BytesIO()
@@ -103,24 +96,18 @@ def draw_single_comparison_chart(detected: int, attendance: int) -> np.ndarray:
103
  return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
104
 
105
 
106
- def run_single_validation(
107
- image: np.ndarray, attendance_count: float, conf_threshold: float
108
  ) -> Tuple[np.ndarray, str, np.ndarray]:
109
- try:
110
- attendance = int(attendance_count)
111
- except (TypeError, ValueError):
112
- attendance = 0
113
-
114
  try:
115
  annotated, detected = infer_image(image, conf_threshold)
116
- status = classify_status(detected, attendance)
117
  summary = (
118
  "### Hasil Klasifikasi\n"
119
- f"- Jumlah terdeteksi: **{detected}**\n"
120
- f"- Jumlah presensi: **{attendance}**\n"
121
  f"- Status klasifikasi: **{status}**"
122
  )
123
- chart = draw_single_comparison_chart(detected, attendance)
124
  return annotated, summary, chart
125
  except Exception as err:
126
  summary = (
@@ -129,181 +116,16 @@ def run_single_validation(
129
  "Tips: pastikan model tersedia (`yolov8n.pt` atau MODEL_PATH)."
130
  )
131
  blank = np.full((320, 500, 3), 245, dtype=np.uint8)
132
- cv2.putText(blank, "Grafik belum tersedia", (120, 165), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (80, 80, 80), 2)
133
- return image, summary, blank
134
-
135
-
136
- def draw_confusion_matrix_image(cm: np.ndarray) -> np.ndarray:
137
- sns.set_style("white")
138
- fig, ax = plt.subplots(figsize=(6.2, 4.8))
139
- sns.heatmap(
140
- cm,
141
- annot=True,
142
- fmt="d",
143
- cmap="Blues",
144
- cbar=True,
145
- xticklabels=CLASS_NAMES,
146
- yticklabels=CLASS_NAMES,
147
- linewidths=0.5,
148
- linecolor="white",
149
- ax=ax,
150
- )
151
- ax.set_title("Confusion Matrix", fontsize=12, pad=10)
152
- ax.set_xlabel("Predicted label")
153
- ax.set_ylabel("True label")
154
- plt.xticks(rotation=30, ha="right")
155
- plt.yticks(rotation=0)
156
- plt.tight_layout()
157
-
158
- buf = io.BytesIO()
159
- fig.savefig(buf, format="png", dpi=150)
160
- plt.close(fig)
161
- buf.seek(0)
162
- data = np.frombuffer(buf.read(), dtype=np.uint8)
163
- img_bgr = cv2.imdecode(data, cv2.IMREAD_COLOR)
164
- return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
165
-
166
-
167
- def _normalize_actual_label(value: str) -> str:
168
- v = str(value).strip().lower()
169
- if v in ("sesuai", "match", "1", "true"):
170
- return "Sesuai"
171
- return "Tidak sesuai"
172
-
173
-
174
- def _get_col(df: pd.DataFrame, options: List[str]) -> str:
175
- lower_map = {c.lower(): c for c in df.columns}
176
- for opt in options:
177
- if opt.lower() in lower_map:
178
- return lower_map[opt.lower()]
179
- raise ValueError(f"Kolom tidak ditemukan. Butuh salah satu dari: {options}")
180
-
181
-
182
- def evaluate_classification_batch(
183
- image_zip_path: str, csv_path: str, conf_threshold: float
184
- ) -> Tuple[np.ndarray, str]:
185
- if not image_zip_path or not csv_path:
186
- raise ValueError("Upload ZIP gambar dan CSV evaluasi terlebih dahulu.")
187
-
188
- df = pd.read_csv(csv_path)
189
- fname_col = _get_col(df, ["filename", "file", "image", "image_name"])
190
- pres_col = _get_col(df, ["presensi", "attendance", "attendance_count"])
191
- actual_col = _get_col(df, ["actual_label", "label", "ground_truth", "aktual"])
192
-
193
- y_true: List[str] = []
194
- y_pred: List[str] = []
195
-
196
- with tempfile.TemporaryDirectory() as tmpdir:
197
- with zipfile.ZipFile(image_zip_path, "r") as zf:
198
- zf.extractall(tmpdir)
199
-
200
- for _, row in df.iterrows():
201
- fname = str(row[fname_col]).strip()
202
- attendance = int(row[pres_col])
203
- actual = _normalize_actual_label(row[actual_col])
204
- img_path = os.path.join(tmpdir, fname)
205
- img = cv2.imread(img_path)
206
- if img is None:
207
- continue
208
- rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
209
- _, detected = infer_image(rgb, conf_threshold)
210
- pred = classify_status(detected, attendance)
211
- y_true.append(actual)
212
- y_pred.append(pred)
213
-
214
- if len(y_true) == 0:
215
- raise ValueError("Tidak ada sampel valid yang berhasil diproses.")
216
-
217
- cm = confusion_matrix(y_true, y_pred, labels=CLASS_NAMES)
218
- cm_img = draw_confusion_matrix_image(cm)
219
- tn, fp, fn, tp = cm.ravel() if cm.size == 4 else (0, 0, 0, 0)
220
- acc = _safe_div(tp + tn, tp + tn + fp + fn)
221
- prec = _safe_div(tp, tp + fp)
222
- rec = _safe_div(tp, tp + fn)
223
- f1 = _safe_div(2 * prec * rec, prec + rec)
224
- metrics = (
225
- "### Hasil Evaluasi Batch\n"
226
- f"- Jumlah sampel: **{len(y_true)}**\n"
227
- f"- Accuracy: **{acc:.4f}**\n"
228
- f"- Precision (Sesuai): **{prec:.4f}**\n"
229
- f"- Recall (Sesuai): **{rec:.4f}**\n"
230
- f"- F1-Score (Sesuai): **{f1:.4f}**"
231
- )
232
- return cm_img, metrics
233
-
234
-
235
- def run_batch_ui(image_zip: Any, csv_file: Any, conf_threshold: float) -> Tuple[np.ndarray, str]:
236
- try:
237
- zip_path = image_zip.name if image_zip is not None else ""
238
- csv_path = csv_file.name if csv_file is not None else ""
239
- return evaluate_classification_batch(zip_path, csv_path, conf_threshold)
240
- except Exception as err:
241
- msg = f"### Hasil Evaluasi Batch\nEvaluasi batch gagal: {err}"
242
- blank = np.full((420, 620, 3), 245, dtype=np.uint8)
243
- cv2.putText(blank, "Confusion Matrix belum tersedia", (95, 210), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (80, 80, 80), 2)
244
- return blank, msg
245
-
246
-
247
- def _find_yaml(root_dir: str, yaml_name_or_path: str) -> str:
248
- candidate = os.path.join(root_dir, yaml_name_or_path)
249
- if os.path.exists(candidate):
250
- return candidate
251
-
252
- target_name = os.path.basename(yaml_name_or_path)
253
- for base, _, files in os.walk(root_dir):
254
- for f in files:
255
- if f == target_name:
256
- return os.path.join(base, f)
257
- raise FileNotFoundError(f"File YAML dataset tidak ditemukan: {yaml_name_or_path}")
258
-
259
-
260
- def run_training_ui(
261
- dataset_zip: Any, data_yaml: str, epochs: float, imgsz: float
262
- ) -> Tuple[str, str]:
263
- if dataset_zip is None:
264
- return "Training gagal: upload ZIP dataset terlebih dahulu.", None
265
-
266
- try:
267
- dataset_path = dataset_zip.name
268
- base_dir = os.path.dirname(__file__)
269
- train_workspace = os.path.join(base_dir, "training_data", datetime.now().strftime("%Y%m%d_%H%M%S"))
270
- os.makedirs(train_workspace, exist_ok=True)
271
-
272
- with zipfile.ZipFile(dataset_path, "r") as zf:
273
- zf.extractall(train_workspace)
274
-
275
- yaml_path = _find_yaml(train_workspace, data_yaml.strip() or "data.yaml")
276
- base_model = os.path.join(base_dir, "yolov8n.pt")
277
- if not os.path.exists(base_model):
278
- base_model = resolve_model_path()
279
-
280
- train_model = YOLO(base_model)
281
- results = train_model.train(
282
- data=yaml_path,
283
- epochs=int(epochs),
284
- imgsz=int(imgsz),
285
- project=os.path.join(base_dir, "runs_train"),
286
- name="space_train",
287
- exist_ok=True,
288
- device="cpu",
289
- verbose=False,
290
- )
291
-
292
- save_dir = str(results.save_dir)
293
- best_path = os.path.join(save_dir, "weights", "best.pt")
294
- if not os.path.exists(best_path):
295
- best_path = None
296
-
297
- msg = (
298
- "Training selesai.\n"
299
- f"- Dataset workspace: `{train_workspace}`\n"
300
- f"- data.yaml: `{yaml_path}`\n"
301
- f"- Output run: `{save_dir}`\n"
302
- f"- best.pt: `{best_path}`"
303
  )
304
- return msg, best_path
305
- except Exception as err:
306
- return f"Training gagal: {err}", None
307
 
308
 
309
  CUSTOM_CSS = """
@@ -312,12 +134,12 @@ CUSTOM_CSS = """
312
  """
313
 
314
 
315
- with gr.Blocks(css=CUSTOM_CSS, title="Klasifikasi Kehadiran Mahasiswa - YOLOv8") as demo:
316
  gr.HTML(
317
  """
318
- <h1 style="text-align:center;margin-bottom:4px;">Klasifikasi Kehadiran Mahasiswa Berbasis YOLOv8</h1>
319
  <p style="text-align:center;color:#4a5568;margin-top:0;">
320
- Status <b>Sesuai</b> jika jumlah terdeteksi = jumlah presensi, selain itu <b>Tidak sesuai</b>.
321
  </p>
322
  """
323
  )
@@ -331,54 +153,20 @@ with gr.Blocks(css=CUSTOM_CSS, title="Klasifikasi Kehadiran Mahasiswa - YOLOv8")
331
  sources=["upload", "webcam"],
332
  )
333
  output_image = gr.Image(label="Hasil Proses (Dekat Upload)", type="numpy")
334
- attendance_input = gr.Number(label="Jumlah Presensi", value=0, precision=0, minimum=0)
335
  conf_input = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Confidence Threshold")
336
- run_btn = gr.Button("Proses Klasifikasi", variant="primary")
337
- gr.Markdown("### Input Evaluasi Batch")
338
- image_zip = gr.File(label="ZIP Gambar Evaluasi", file_types=[".zip"])
339
- csv_file = gr.File(label="CSV Ground Truth", file_types=[".csv"])
340
- eval_btn = gr.Button("Jalankan Evaluasi Batch", variant="secondary")
341
 
342
  with gr.Column(elem_classes=["section-box"]):
343
  gr.Markdown("## Section 2 - Hasil Processing")
344
  class_result_md = gr.Markdown("Hasil klasifikasi akan tampil di sini.")
345
  comparison_chart = gr.Image(label="Grafik Klasifikasi", type="numpy")
346
- cm_image = gr.Image(label="Confusion Matrix", type="numpy")
347
- eval_metrics = gr.Markdown("Metrik evaluasi batch akan tampil di sini.")
348
-
349
- with gr.Column(elem_classes=["section-box"]):
350
- gr.Markdown("## Section 3 - Training")
351
- gr.Markdown(
352
- "Upload ZIP dataset YOLO lalu jalankan training. "
353
- "Isi `data.yaml` sesuai nama/path file YAML di dalam ZIP."
354
- )
355
- train_zip = gr.File(label="ZIP Dataset Training", file_types=[".zip"])
356
- train_yaml = gr.Textbox(label="Path data.yaml di dalam ZIP", value="data.yaml")
357
- with gr.Row():
358
- train_epochs = gr.Number(label="Epochs", value=20, precision=0, minimum=1)
359
- train_imgsz = gr.Number(label="Image Size", value=640, precision=0, minimum=64)
360
- train_btn = gr.Button("Mulai Training", variant="primary")
361
- train_log = gr.Textbox(label="Log Training", lines=7, interactive=False)
362
- train_best = gr.File(label="Download best.pt (jika tersedia)")
363
 
364
  run_btn.click(
365
- fn=run_single_validation,
366
- inputs=[image_input, attendance_input, conf_input],
367
  outputs=[output_image, class_result_md, comparison_chart],
368
  )
369
 
370
- eval_btn.click(
371
- fn=run_batch_ui,
372
- inputs=[image_zip, csv_file, conf_input],
373
- outputs=[cm_image, eval_metrics],
374
- )
375
-
376
- train_btn.click(
377
- fn=run_training_ui,
378
- inputs=[train_zip, train_yaml, train_epochs, train_imgsz],
379
- outputs=[train_log, train_best],
380
- )
381
-
382
 
383
  if __name__ == "__main__":
384
  demo.launch()
 
1
  import io
2
  import os
 
 
 
3
  from functools import lru_cache
4
+ from typing import Any, Tuple
5
 
6
  import cv2
7
  import gradio as gr
8
  import matplotlib.pyplot as plt
9
  import numpy as np
 
 
10
  from huggingface_hub import hf_hub_download
 
11
  from ultralytics import YOLO
12
 
13
 
 
 
 
14
  def resolve_model_path() -> str:
15
  model_env = os.getenv("MODEL_PATH", "").strip()
16
  base_dir = os.path.dirname(__file__)
 
40
  return YOLO(resolve_model_path())
41
 
42
 
 
 
 
 
43
  def _resolve_person_class_id(result: Any) -> int:
44
  names = getattr(result, "names", {})
45
  if isinstance(names, dict):
 
66
  return annotated, detected_count
67
 
68
 
69
+ def classify_presence(detected_count: int) -> str:
70
+ return "Ada Mahasiswa" if detected_count > 0 else "Tidak Ada Mahasiswa"
71
 
72
 
73
+ def draw_detection_chart(detected: int) -> np.ndarray:
74
  fig, ax = plt.subplots(figsize=(5.0, 3.2))
75
+ labels = ["Jumlah Person Terdeteksi"]
76
+ values = [detected]
77
+ bars = ax.bar(labels, values, color=["#2b6cb0"])
78
+ ax.set_title("Grafik Hasil Pengecekan")
79
  ax.set_ylabel("Jumlah")
80
  for b in bars:
81
+ ax.text(
82
+ b.get_x() + b.get_width() / 2,
83
+ b.get_height() + 0.05,
84
+ str(int(b.get_height())),
85
+ ha="center",
86
+ va="bottom",
87
+ )
88
  plt.tight_layout()
89
 
90
  buf = io.BytesIO()
 
96
  return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
97
 
98
 
99
+ def run_check_and_classify(
100
+ image: np.ndarray, conf_threshold: float
101
  ) -> Tuple[np.ndarray, str, np.ndarray]:
 
 
 
 
 
102
  try:
103
  annotated, detected = infer_image(image, conf_threshold)
104
+ status = classify_presence(detected)
105
  summary = (
106
  "### Hasil Klasifikasi\n"
107
+ f"- Jumlah person terdeteksi: **{detected}**\n"
 
108
  f"- Status klasifikasi: **{status}**"
109
  )
110
+ chart = draw_detection_chart(detected)
111
  return annotated, summary, chart
112
  except Exception as err:
113
  summary = (
 
116
  "Tips: pastikan model tersedia (`yolov8n.pt` atau MODEL_PATH)."
117
  )
118
  blank = np.full((320, 500, 3), 245, dtype=np.uint8)
119
+ cv2.putText(
120
+ blank,
121
+ "Grafik belum tersedia",
122
+ (120, 165),
123
+ cv2.FONT_HERSHEY_SIMPLEX,
124
+ 0.8,
125
+ (80, 80, 80),
126
+ 2,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  )
128
+ return image, summary, blank
 
 
129
 
130
 
131
  CUSTOM_CSS = """
 
134
  """
135
 
136
 
137
+ with gr.Blocks(css=CUSTOM_CSS, title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
138
  gr.HTML(
139
  """
140
+ <h1 style="text-align:center;margin-bottom:4px;">Pengecekan dan Klasifikasi Kehadiran Berbasis YOLOv8</h1>
141
  <p style="text-align:center;color:#4a5568;margin-top:0;">
142
+ Mode saat ini: deteksi person pada gambar kelas + klasifikasi keberadaan mahasiswa.
143
  </p>
144
  """
145
  )
 
153
  sources=["upload", "webcam"],
154
  )
155
  output_image = gr.Image(label="Hasil Proses (Dekat Upload)", type="numpy")
 
156
  conf_input = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Confidence Threshold")
157
+ run_btn = gr.Button("Proses Pengecekan", variant="primary")
 
 
 
 
158
 
159
  with gr.Column(elem_classes=["section-box"]):
160
  gr.Markdown("## Section 2 - Hasil Processing")
161
  class_result_md = gr.Markdown("Hasil klasifikasi akan tampil di sini.")
162
  comparison_chart = gr.Image(label="Grafik Klasifikasi", type="numpy")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  run_btn.click(
165
+ fn=run_check_and_classify,
166
+ inputs=[image_input, conf_input],
167
  outputs=[output_image, class_result_md, comparison_chart],
168
  )
169
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  if __name__ == "__main__":
172
  demo.launch()
requirements.txt CHANGED
@@ -3,7 +3,4 @@ ultralytics>=8.2.0
3
  opencv-python-headless>=4.10.0.84
4
  huggingface_hub>=0.23.0
5
  numpy>=1.26.0
6
- pandas>=2.2.0
7
- scikit-learn>=1.5.0
8
  matplotlib>=3.8.0
9
- seaborn>=0.13.0
 
3
  opencv-python-headless>=4.10.0.84
4
  huggingface_hub>=0.23.0
5
  numpy>=1.26.0
 
 
6
  matplotlib>=3.8.0