firgiawann commited on
Commit
4a15b42
·
verified ·
1 Parent(s): 52c4156

Revise layout evaluation and training artifact sync update 32

Browse files
Files changed (1) hide show
  1. app.py +289 -106
app.py CHANGED
@@ -21,10 +21,10 @@ import seaborn as sns
21
  from huggingface_hub import CommitOperationAdd, CommitOperationDelete, HfApi, hf_hub_download
22
  from ultralytics import YOLO
23
 
24
- CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
25
  EVAL_CLASSES = ["Person", "Not Person/Background"]
26
  APP_VERSION = os.getenv("APP_VERSION", "v0.9.0")
27
- APP_MINOR_UPDATE = os.getenv("APP_MINOR_UPDATE", "31")
28
 
29
 
30
  # Gradio/Spaces can trigger noisy asyncio cleanup traces ("Invalid file descriptor: -1")
@@ -374,13 +374,13 @@ def save_detection_artifacts(
374
  return str(image_path), str(csv_path), hf_sync_msg
375
 
376
 
377
- def draw_detection_chart(detected: int) -> np.ndarray:
378
- fig, ax = plt.subplots(figsize=(5.0, 3.2))
379
- labels = ["Jumlah Person Terdeteksi"]
380
- values = [detected]
381
- bars = ax.bar(labels, values, color=["#2b6cb0"])
382
  ax.set_title("Grafik Hasil Pengecekan")
383
- ax.set_ylabel("Jumlah")
384
  for b in bars:
385
  ax.text(
386
  b.get_x() + b.get_width() / 2,
@@ -400,21 +400,33 @@ def draw_detection_chart(detected: int) -> np.ndarray:
400
  return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
401
 
402
 
403
- def draw_compare_chart(detected_before: int, detected_after: int) -> np.ndarray:
404
- fig, ax = plt.subplots(figsize=(6.2, 3.4))
405
- labels = ["YOLOv8n", "Best.pt"]
406
- values = [detected_before, detected_after]
407
- bars = ax.bar(labels, values, color=["#2b6cb0", "#805ad5"])
408
- ax.set_title("Perbandingan Sebelum vs Sesudah Training")
409
- ax.set_ylabel("Jumlah")
410
- for b in bars:
411
- ax.text(
412
- b.get_x() + b.get_width() / 2,
413
- b.get_height() + 0.05,
414
- str(int(b.get_height())),
415
- ha="center",
416
- va="bottom",
417
- )
 
 
 
 
 
 
 
 
 
 
 
 
418
  plt.tight_layout()
419
  buf = io.BytesIO()
420
  fig.savefig(buf, format="png", dpi=140)
@@ -437,6 +449,43 @@ def _merge_annotated_images(left: np.ndarray, right: np.ndarray) -> np.ndarray:
437
  return combined
438
 
439
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
  def run_check_and_classify(
441
  image: np.ndarray,
442
  conf_threshold: float,
@@ -450,6 +499,7 @@ def run_check_and_classify(
450
  np.ndarray,
451
  str,
452
  str,
 
453
  str,
454
  str | None,
455
  str | None,
@@ -466,7 +516,7 @@ def run_check_and_classify(
466
  )
467
  pred_before = classify_person_vs_background(det_before)
468
  pred_after = classify_person_vs_background(det_after)
469
- chart = draw_compare_chart(det_before, det_after)
470
  yolo_image_path, saved_csv_path, hf_sync_msg_yolo = save_detection_artifacts(
471
  ann_before,
472
  model_choice="YOLOv8n",
@@ -486,6 +536,14 @@ def run_check_and_classify(
486
  non_person_count=non_person_after,
487
  )
488
  actual_label = classify_scene_label(det_before, non_person_before)
 
 
 
 
 
 
 
 
489
  summary = (
490
  "### Ringkasan Perbandingan Model\n"
491
  f"- Confidence threshold: **{conf_threshold:.2f}**\n"
@@ -504,6 +562,7 @@ def run_check_and_classify(
504
  chart,
505
  pred_after,
506
  actual_label,
 
507
  pred_md,
508
  best_image_path,
509
  saved_csv_path,
@@ -516,6 +575,8 @@ def run_check_and_classify(
516
  status_presence = classify_presence(detected)
517
  predicted_label = classify_person_vs_background(detected)
518
  actual_label = classify_scene_label(detected, non_person_count)
 
 
519
  yolo_panel_output: str | np.ndarray | None = None
520
  best_panel_output: str | np.ndarray | None = None
521
 
@@ -524,6 +585,8 @@ def run_check_and_classify(
524
  image, conf_threshold, "YOLOv8n", annotation_style
525
  )
526
  actual_label = classify_scene_label(ref_person_count, ref_non_person_count)
 
 
527
 
528
  saved_image_path, saved_csv_path, hf_sync_msg = save_detection_artifacts(
529
  annotated,
@@ -553,7 +616,15 @@ def run_check_and_classify(
553
  f"- Arsip evaluasi lokal: **{saved_csv_path or '-'}**\n"
554
  f"- Auto-save ke Space: **{hf_sync_msg}**"
555
  )
556
- chart = draw_detection_chart(detected)
 
 
 
 
 
 
 
 
557
  pred_md = f"Label prediksi evaluasi: **{predicted_label}**"
558
  return (
559
  yolo_panel_output,
@@ -562,6 +633,7 @@ def run_check_and_classify(
562
  chart,
563
  predicted_label,
564
  actual_label,
 
565
  pred_md,
566
  saved_image_path,
567
  saved_csv_path,
@@ -582,7 +654,7 @@ def run_check_and_classify(
582
  (80, 80, 80),
583
  2,
584
  )
585
- return None, None, summary, blank, "", "", "Label prediksi evaluasi: **-**", None, None
586
 
587
 
588
  def toggle_model_selector(mode: str):
@@ -601,26 +673,39 @@ def draw_confusion_matrix_from_state(cm_state: Dict[str, int]) -> np.ndarray:
601
  tn = int(cm_state.get("tn", 0))
602
  cm = np.array([[tp, fn], [fp, tn]], dtype=int)
603
 
 
 
 
604
  sns.set_style("white")
605
- fig, ax = plt.subplots(figsize=(6.0, 4.6))
606
  sns.heatmap(
607
  cm,
608
  annot=True,
609
  fmt="d",
610
  cmap="Blues",
611
  cbar=True,
612
- xticklabels=EVAL_CLASSES,
613
- yticklabels=EVAL_CLASSES,
614
  linewidths=0.5,
615
  linecolor="white",
 
616
  ax=ax,
617
  )
618
- ax.set_title("Confusion Matrix")
619
  ax.set_xlabel("Prediksi")
620
  ax.set_ylabel("Aktual")
621
- plt.xticks(rotation=25, ha="right")
 
 
 
 
 
 
 
 
 
622
  plt.yticks(rotation=0)
623
- plt.tight_layout()
624
 
625
  buf = io.BytesIO()
626
  fig.savefig(buf, format="png", dpi=150)
@@ -640,21 +725,25 @@ def render_metric_markdown(cm_state: Dict[str, int]) -> str:
640
  prec = _safe_div(tp, tp + fp)
641
  rec = _safe_div(tp, tp + fn)
642
  f1 = _safe_div(2 * prec * rec, prec + rec)
 
 
 
643
  return (
644
- "### Evaluasi dan Rumus\n"
645
- "- Kelas positif evaluasi: **Person**\n"
646
- f"- Accuracy = (TP+TN)/(TP+FP+FN+TN) = ({tp}+{tn})/({tp+fp+fn+tn}) = **{acc:.4f}**\n"
647
- f"- Precision = TP/(TP+FP) = {tp}/({tp+fp}) = **{prec:.4f}**\n"
648
- f"- Recall = TP/(TP+FN) = {tp}/({tp+fn}) = **{rec:.4f}**\n"
649
- f"- F1-Score = 2PR/(P+R) = **{f1:.4f}**"
 
 
 
650
  )
651
 
652
 
653
- def update_evaluation(
654
- cm_state: Dict[str, int], predicted_label: str, actual_label: str
655
- ) -> Tuple[Dict[str, int], np.ndarray, str, str]:
656
  state = dict(cm_state or CM_INIT)
657
- if predicted_label not in EVAL_CLASSES:
658
  return (
659
  state,
660
  draw_confusion_matrix_from_state(state),
@@ -662,27 +751,25 @@ def update_evaluation(
662
  "Jalankan deteksi terlebih dahulu agar label prediksi tersedia.",
663
  )
664
 
665
- if actual_label == "Person" and predicted_label == "Person":
666
- state["tp"] += 1
667
- elif actual_label == "Person" and predicted_label == "Not Person/Background":
668
- state["fn"] += 1
669
- elif actual_label == "Not Person/Background" and predicted_label == "Person":
670
- state["fp"] += 1
671
- else:
672
- state["tn"] += 1
673
 
674
  return (
675
  state,
676
  draw_confusion_matrix_from_state(state),
677
  render_metric_markdown(state),
678
- "Confusion matrix berhasil diperbarui.",
 
 
 
679
  )
680
 
681
 
682
  def update_evaluation_auto(
683
- cm_state: Dict[str, int], predicted_label: str, actual_label: str
684
  ) -> Tuple[Dict[str, int], np.ndarray, str, str]:
685
- if predicted_label not in EVAL_CLASSES:
686
  state = dict(cm_state or CM_INIT)
687
  return (
688
  state,
@@ -690,9 +777,7 @@ def update_evaluation_auto(
690
  render_metric_markdown(state),
691
  "Jalankan deteksi terlebih dahulu agar evaluasi otomatis dapat diperbarui.",
692
  )
693
- if actual_label not in EVAL_CLASSES:
694
- actual_label = predicted_label
695
- return update_evaluation(cm_state, predicted_label, actual_label)
696
 
697
 
698
  def reset_evaluation() -> Tuple[Dict[str, int], np.ndarray, str, str]:
@@ -959,6 +1044,15 @@ def save_training_curve_image(chart_rgb: np.ndarray, output_path: Path) -> str |
959
  return str(output_path)
960
 
961
 
 
 
 
 
 
 
 
 
 
962
  def save_training_summary(paths: Dict[str, Path], state: Dict[str, Any], rows_count: int) -> str:
963
  payload = {
964
  "saved_at": datetime.now().isoformat(),
@@ -975,6 +1069,49 @@ def save_training_summary(paths: Dict[str, Path], state: Dict[str, Any], rows_co
975
  return str(paths["summary"])
976
 
977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  def _env_true(var_name: str, default: bool = False) -> bool:
979
  val = os.getenv(var_name)
980
  if val is None:
@@ -1284,10 +1421,14 @@ def _training_status_md(state: Dict[str, Any], rows_count: int) -> str:
1284
  f"Tahap: **{stage}** \nEpoch tercatat: **{rows_count}**"
1285
  )
1286
  if status == "done":
1287
- return f"Status training: **Selesai** \nRun: `{run_name}`"
 
 
1288
  if status == "failed":
1289
  err = state.get("error", "-")
1290
- return f"Status training: **Gagal** \nRun: `{run_name}` \nError: `{err}`"
 
 
1291
  return f"Status training: **{status}** \nRun: `{run_name}`"
1292
 
1293
 
@@ -1300,10 +1441,17 @@ def poll_training_status() -> Tuple[str, Any, Any, Any, Any, np.ndarray, str]:
1300
 
1301
  results_csv = _safe_results_csv_path(state)
1302
  rows = _read_results_rows(results_csv) if results_csv is not None else []
1303
- chart = plot_training_curves(results_csv) if results_csv is not None else _make_info_image(
1304
- "Grafik training belum tersedia."
 
 
 
 
 
 
 
 
1305
  )
1306
- chart_file = save_training_curve_image(chart, paths["chart"]) if results_csv is not None else None
1307
 
1308
  if state.get("status") == "running":
1309
  pid = int(state.get("pid", 0) or 0)
@@ -1318,6 +1466,17 @@ def poll_training_status() -> Tuple[str, Any, Any, Any, Any, np.ndarray, str]:
1318
  metrics_file = str(results_csv) if results_csv is not None else None
1319
  log_file = str(paths["log"]) if paths["log"].exists() else None
1320
  summary_file = save_training_summary(paths, state, len(rows))
 
 
 
 
 
 
 
 
 
 
 
1321
  return (
1322
  log_text,
1323
  best_file,
@@ -1340,6 +1499,7 @@ def poll_training_status() -> Tuple[str, Any, Any, Any, Any, np.ndarray, str]:
1340
 
1341
 
1342
  def run_training(
 
1343
  epochs: float,
1344
  imgsz: float,
1345
  batch: float,
@@ -1377,7 +1537,10 @@ def run_training(
1377
  log_file=paths["log"],
1378
  )
1379
 
1380
- model_path = str(base_dir / "yolov8n.pt") if (base_dir / "yolov8n.pt").exists() else resolve_model_path("YOLOv8n")
 
 
 
1381
  run_name = f"classroom_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
1382
  project_dir = base_dir / "runs_train"
1383
  save_dir = project_dir / run_name
@@ -1385,6 +1548,7 @@ def run_training(
1385
 
1386
  with paths["log"].open("a", encoding="utf-8") as lf:
1387
  lf.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Start training {run_name}\n")
 
1388
  lf.write(f"{prep_info}\n")
1389
 
1390
  train_code = r"""
@@ -1532,6 +1696,9 @@ CUSTOM_CSS = """
1532
  }
1533
  .version-foot {font-size:12px; color:#64748b; text-align:center; margin-top:8px;}
1534
  .advanced-box {margin-top: 8px; padding: 8px; border: 1px dashed #cbd5e1; border-radius: 8px;}
 
 
 
1535
 
1536
  @media (prefers-color-scheme: dark) {
1537
  body, .gradio-container {background: #0f172a !important; color: #e2e8f0 !important;}
@@ -1541,6 +1708,8 @@ CUSTOM_CSS = """
1541
  .version-chip {background: #111827 !important; border-color: #475569 !important; color: #cbd5e1 !important;}
1542
  .version-foot {color: #94a3b8 !important;}
1543
  .advanced-box {border-color: #475569 !important;}
 
 
1544
  }
1545
  """
1546
 
@@ -1574,7 +1743,7 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1574
  )
1575
 
1576
  with gr.Column(elem_classes=["section-box"]):
1577
- gr.Markdown("## Tahap 1 - Akuisisi Data", elem_classes=["section-title"])
1578
  inference_mode = gr.Dropdown(
1579
  choices=["Single Model", "Bandingkan (YOLOv8n vs Best.pt)"],
1580
  value="Single Model",
@@ -1585,11 +1754,6 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1585
  value="YOLOv8n",
1586
  label="Pilih Model",
1587
  )
1588
- image_input = gr.Image(
1589
- label="Input Gambar Kelas (Upload/Kamera)",
1590
- type="numpy",
1591
- sources=["upload", "webcam"],
1592
- )
1593
  conf_input = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Parameter Confidence (Deteksi)")
1594
  with gr.Accordion("Advanced Options", open=False, elem_classes=["advanced-box"]):
1595
  annotation_style = gr.Radio(
@@ -1598,53 +1762,35 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1598
  label="Gaya anotasi objek",
1599
  info="Pilih tampilan bbox atau label-only untuk label objek yang terdeteksi.",
1600
  )
1601
- run_btn = gr.Button("Jalankan Deteksi dan Klasifikasi", variant="primary")
1602
 
1603
  with gr.Column(elem_classes=["section-box"]):
1604
- gr.Markdown("## Tahap 2 - Hasil Deteksi dan Klasifikasi", elem_classes=["section-title"])
1605
- output_image_yolo = gr.Image(
1606
- label="Output Deteksi YOLOv8n",
1607
- type="filepath",
1608
- )
1609
- output_image_best = gr.Image(
1610
- label="Output Deteksi Best.pt",
1611
- type="filepath",
1612
  )
1613
- class_result_md = gr.Markdown("Ringkasan hasil deteksi akan tampil di sini.")
1614
- comparison_chart = gr.Image(label="Visualisasi Jumlah Person Terdeteksi", type="numpy")
1615
- predicted_label_md = gr.Markdown("Label prediksi evaluasi: **-**")
1616
- saved_detection_file = gr.File(label="File Gambar Hasil Evaluasi")
1617
- evaluation_csv_file = gr.File(label="Log CSV Evaluasi Lokal")
1618
 
1619
  with gr.Column(elem_classes=["section-box"]):
1620
- gr.Markdown("## Tahap 3 - Evaluasi (Confusion Matrix + Rumus)", elem_classes=["section-title"])
1621
- cm_state = gr.State(dict(CM_INIT))
1622
- predicted_state = gr.State("")
1623
- actual_state = gr.State("")
1624
- gr.Markdown(
1625
- "Evaluasi diperbarui otomatis setelah inferensi. "
1626
- "Untuk mode `Best.pt` dan mode perbandingan, label aktual/proxy diambil dari referensi scene YOLOv8n "
1627
- "agar kasus objek non-person ikut masuk sebagai `Not Person/Background`."
1628
  )
1629
- with gr.Row():
1630
- reset_eval_btn = gr.Button("Reset Evaluasi")
1631
- eval_status = gr.Markdown("Belum ada update evaluasi otomatis.")
1632
- with gr.Row():
1633
- cm_image = gr.Image(
1634
- label="Confusion Matrix",
1635
- value=draw_confusion_matrix_from_state(CM_INIT),
1636
- type="numpy",
1637
- interactive=False,
1638
- )
1639
- metric_md = gr.Markdown(render_metric_markdown(CM_INIT))
1640
-
1641
- with gr.Column(elem_classes=["section-box"]):
1642
- with gr.Accordion("Tahap 4 - Pengembangan Model (Training)", open=False):
1643
  gr.Markdown(
1644
  "Training akan membaca dataset dari `dataset/train` dan `dataset/test`.\n"
1645
  "Jika label `.txt` belum ada, aktifkan pseudo-label agar proses tetap jalan.\n"
1646
  "Saat training berjalan, terminal progres dan grafik epoch akan update otomatis."
1647
  )
 
 
 
 
 
 
1648
  with gr.Row():
1649
  train_epochs = gr.Number(label="Epochs", value=10, precision=0, minimum=1)
1650
  train_imgsz = gr.Number(label="Image Size", value=640, precision=0, minimum=64)
@@ -1674,6 +1820,42 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1674
  training_log_file = gr.File(label="training.log / Log Training Lokal")
1675
  training_chart_file = gr.File(label="training_curve_latest.png / Grafik Training Lokal")
1676
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1677
  run_btn_evt = run_btn.click(
1678
  fn=run_check_and_classify,
1679
  inputs=[image_input, conf_input, model_choice, inference_mode, annotation_style],
@@ -1684,6 +1866,7 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1684
  comparison_chart,
1685
  predicted_state,
1686
  actual_state,
 
1687
  predicted_label_md,
1688
  saved_detection_file,
1689
  evaluation_csv_file,
@@ -1691,7 +1874,7 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1691
  )
1692
  run_btn_evt.then(
1693
  fn=update_evaluation_auto,
1694
- inputs=[cm_state, predicted_state, actual_state],
1695
  outputs=[cm_state, cm_image, metric_md, eval_status],
1696
  )
1697
  inference_mode.change(
@@ -1708,7 +1891,7 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1708
 
1709
  train_btn.click(
1710
  fn=run_training,
1711
- inputs=[train_epochs, train_imgsz, train_batch, use_pseudo, pseudo_conf],
1712
  outputs=[
1713
  train_log,
1714
  best_model_file,
 
21
  from huggingface_hub import CommitOperationAdd, CommitOperationDelete, HfApi, hf_hub_download
22
  from ultralytics import YOLO
23
 
24
+ CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0, "person": 0, "not_person": 0}
25
  EVAL_CLASSES = ["Person", "Not Person/Background"]
26
  APP_VERSION = os.getenv("APP_VERSION", "v0.9.0")
27
+ APP_MINOR_UPDATE = os.getenv("APP_MINOR_UPDATE", "32")
28
 
29
 
30
  # Gradio/Spaces can trigger noisy asyncio cleanup traces ("Invalid file descriptor: -1")
 
374
  return str(image_path), str(csv_path), hf_sync_msg
375
 
376
 
377
+ def draw_detection_chart(person_count: int, non_person_count: int) -> np.ndarray:
378
+ fig, ax = plt.subplots(figsize=(6.2, 3.6))
379
+ labels = ["Person", "Not Person"]
380
+ values = [person_count, non_person_count]
381
+ bars = ax.bar(labels, values, color=["#2563eb", "#64748b"])
382
  ax.set_title("Grafik Hasil Pengecekan")
383
+ ax.set_ylabel("Jumlah Objek")
384
  for b in bars:
385
  ax.text(
386
  b.get_x() + b.get_width() / 2,
 
400
  return cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
401
 
402
 
403
+ def draw_compare_chart(
404
+ yolo_person: int,
405
+ yolo_non_person: int,
406
+ best_person: int,
407
+ best_non_person: int,
408
+ ) -> np.ndarray:
409
+ fig, ax = plt.subplots(figsize=(7.4, 3.8))
410
+ x = np.arange(2)
411
+ width = 0.36
412
+ person_values = [yolo_person, best_person]
413
+ non_person_values = [yolo_non_person, best_non_person]
414
+ bars_person = ax.bar(x - width / 2, person_values, width, label="Person", color="#2563eb")
415
+ bars_non_person = ax.bar(x + width / 2, non_person_values, width, label="Not Person", color="#64748b")
416
+ ax.set_title("Perbandingan Count Objek per Model")
417
+ ax.set_ylabel("Jumlah Objek")
418
+ ax.set_xticks(x)
419
+ ax.set_xticklabels(["YOLOv8n", "Best.pt"])
420
+ ax.legend(loc="best")
421
+ for bars in (bars_person, bars_non_person):
422
+ for b in bars:
423
+ ax.text(
424
+ b.get_x() + b.get_width() / 2,
425
+ b.get_height() + 0.05,
426
+ str(int(b.get_height())),
427
+ ha="center",
428
+ va="bottom",
429
+ )
430
  plt.tight_layout()
431
  buf = io.BytesIO()
432
  fig.savefig(buf, format="png", dpi=140)
 
449
  return combined
450
 
451
 
452
+ def build_eval_payload(
453
+ predicted_label: str,
454
+ actual_label: str,
455
+ predicted_person_count: int,
456
+ predicted_non_person_count: int,
457
+ actual_person_count: int,
458
+ actual_non_person_count: int,
459
+ ) -> Dict[str, Any]:
460
+ return {
461
+ "predicted_label": predicted_label,
462
+ "actual_label": actual_label,
463
+ "predicted_person_count": int(predicted_person_count),
464
+ "predicted_non_person_count": int(predicted_non_person_count),
465
+ "actual_person_count": int(actual_person_count),
466
+ "actual_non_person_count": int(actual_non_person_count),
467
+ }
468
+
469
+
470
+ def object_confusion_delta(eval_payload: Dict[str, Any]) -> Dict[str, int]:
471
+ actual_person = int(eval_payload.get("actual_person_count", 0) or 0)
472
+ actual_non_person = int(eval_payload.get("actual_non_person_count", 0) or 0)
473
+ predicted_person = int(eval_payload.get("predicted_person_count", 0) or 0)
474
+
475
+ tp = min(actual_person, predicted_person)
476
+ fn = max(actual_person - predicted_person, 0)
477
+ fp = max(predicted_person - actual_person, 0)
478
+ tn = max(actual_non_person - fp, 0)
479
+ return {
480
+ "tp": tp,
481
+ "fp": fp,
482
+ "fn": fn,
483
+ "tn": tn,
484
+ "person": actual_person,
485
+ "not_person": actual_non_person,
486
+ }
487
+
488
+
489
  def run_check_and_classify(
490
  image: np.ndarray,
491
  conf_threshold: float,
 
499
  np.ndarray,
500
  str,
501
  str,
502
+ Dict[str, Any],
503
  str,
504
  str | None,
505
  str | None,
 
516
  )
517
  pred_before = classify_person_vs_background(det_before)
518
  pred_after = classify_person_vs_background(det_after)
519
+ chart = draw_compare_chart(det_before, non_person_before, det_after, non_person_after)
520
  yolo_image_path, saved_csv_path, hf_sync_msg_yolo = save_detection_artifacts(
521
  ann_before,
522
  model_choice="YOLOv8n",
 
536
  non_person_count=non_person_after,
537
  )
538
  actual_label = classify_scene_label(det_before, non_person_before)
539
+ eval_payload = build_eval_payload(
540
+ predicted_label=pred_after,
541
+ actual_label=actual_label,
542
+ predicted_person_count=det_after,
543
+ predicted_non_person_count=non_person_after,
544
+ actual_person_count=det_before,
545
+ actual_non_person_count=non_person_before,
546
+ )
547
  summary = (
548
  "### Ringkasan Perbandingan Model\n"
549
  f"- Confidence threshold: **{conf_threshold:.2f}**\n"
 
562
  chart,
563
  pred_after,
564
  actual_label,
565
+ eval_payload,
566
  pred_md,
567
  best_image_path,
568
  saved_csv_path,
 
575
  status_presence = classify_presence(detected)
576
  predicted_label = classify_person_vs_background(detected)
577
  actual_label = classify_scene_label(detected, non_person_count)
578
+ actual_person_count = detected
579
+ actual_non_person_count = non_person_count
580
  yolo_panel_output: str | np.ndarray | None = None
581
  best_panel_output: str | np.ndarray | None = None
582
 
 
585
  image, conf_threshold, "YOLOv8n", annotation_style
586
  )
587
  actual_label = classify_scene_label(ref_person_count, ref_non_person_count)
588
+ actual_person_count = ref_person_count
589
+ actual_non_person_count = ref_non_person_count
590
 
591
  saved_image_path, saved_csv_path, hf_sync_msg = save_detection_artifacts(
592
  annotated,
 
616
  f"- Arsip evaluasi lokal: **{saved_csv_path or '-'}**\n"
617
  f"- Auto-save ke Space: **{hf_sync_msg}**"
618
  )
619
+ chart = draw_detection_chart(detected, non_person_count)
620
+ eval_payload = build_eval_payload(
621
+ predicted_label=predicted_label,
622
+ actual_label=actual_label,
623
+ predicted_person_count=detected,
624
+ predicted_non_person_count=non_person_count,
625
+ actual_person_count=actual_person_count,
626
+ actual_non_person_count=actual_non_person_count,
627
+ )
628
  pred_md = f"Label prediksi evaluasi: **{predicted_label}**"
629
  return (
630
  yolo_panel_output,
 
633
  chart,
634
  predicted_label,
635
  actual_label,
636
+ eval_payload,
637
  pred_md,
638
  saved_image_path,
639
  saved_csv_path,
 
654
  (80, 80, 80),
655
  2,
656
  )
657
+ return None, None, summary, blank, "", "", {}, "Label prediksi evaluasi: **-**", None, None
658
 
659
 
660
  def toggle_model_selector(mode: str):
 
673
  tn = int(cm_state.get("tn", 0))
674
  cm = np.array([[tp, fn], [fp, tn]], dtype=int)
675
 
676
+ person_total = int(cm_state.get("person", tp + fn))
677
+ not_person_total = int(cm_state.get("not_person", fp + tn))
678
+
679
  sns.set_style("white")
680
+ fig, ax = plt.subplots(figsize=(7.4, 5.8))
681
  sns.heatmap(
682
  cm,
683
  annot=True,
684
  fmt="d",
685
  cmap="Blues",
686
  cbar=True,
687
+ xticklabels=["Prediksi Person", "Prediksi Not Person"],
688
+ yticklabels=["Aktual Person", "Aktual Not Person"],
689
  linewidths=0.5,
690
  linecolor="white",
691
+ annot_kws={"size": 14, "weight": "bold"},
692
  ax=ax,
693
  )
694
+ ax.set_title("Confusion Matrix Count Objek")
695
  ax.set_xlabel("Prediksi")
696
  ax.set_ylabel("Aktual")
697
+ ax.text(
698
+ 0.5,
699
+ -0.22,
700
+ f"Total aktual: Person={person_total} | Not Person/Background={not_person_total}",
701
+ transform=ax.transAxes,
702
+ ha="center",
703
+ va="top",
704
+ fontsize=10,
705
+ )
706
+ plt.xticks(rotation=12, ha="right")
707
  plt.yticks(rotation=0)
708
+ plt.tight_layout(rect=[0, 0.07, 1, 1])
709
 
710
  buf = io.BytesIO()
711
  fig.savefig(buf, format="png", dpi=150)
 
725
  prec = _safe_div(tp, tp + fp)
726
  rec = _safe_div(tp, tp + fn)
727
  f1 = _safe_div(2 * prec * rec, prec + rec)
728
+ person_total = int(cm_state.get("person", tp + fn))
729
+ not_person_total = int(cm_state.get("not_person", fp + tn))
730
+ total = tp + fp + fn + tn
731
  return (
732
+ "### Rumus Evaluasi Berbasis Count\n"
733
+ f"**Count objek aktual:** Person = `{person_total}`, Not Person/Background = `{not_person_total}` \n"
734
+ f"**TP:** `{tp}` | **FP:** `{fp}` | **FN:** `{fn}` | **TN:** `{tn}` | **Total:** `{total}`\n\n"
735
+ "| Metrik | Rumus | Substitusi | Hasil |\n"
736
+ "|---|---:|---:|---:|\n"
737
+ f"| Accuracy | `(TP+TN)/(TP+FP+FN+TN)` | `({tp}+{tn})/({total})` | **{acc:.4f}** |\n"
738
+ f"| Precision | `TP/(TP+FP)` | `{tp}/({tp+fp})` | **{prec:.4f}** |\n"
739
+ f"| Recall | `TP/(TP+FN)` | `{tp}/({tp+fn})` | **{rec:.4f}** |\n"
740
+ f"| F1-Score | `2*(Precision*Recall)/(Precision+Recall)` | `2*({prec:.4f}*{rec:.4f})/({prec+rec:.4f})` | **{f1:.4f}** |"
741
  )
742
 
743
 
744
+ def update_evaluation(cm_state: Dict[str, int], eval_payload: Dict[str, Any]) -> Tuple[Dict[str, int], np.ndarray, str, str]:
 
 
745
  state = dict(cm_state or CM_INIT)
746
+ if not eval_payload:
747
  return (
748
  state,
749
  draw_confusion_matrix_from_state(state),
 
751
  "Jalankan deteksi terlebih dahulu agar label prediksi tersedia.",
752
  )
753
 
754
+ delta = object_confusion_delta(eval_payload)
755
+ for key in ("tp", "fp", "fn", "tn", "person", "not_person"):
756
+ state[key] = int(state.get(key, 0)) + int(delta.get(key, 0))
 
 
 
 
 
757
 
758
  return (
759
  state,
760
  draw_confusion_matrix_from_state(state),
761
  render_metric_markdown(state),
762
+ (
763
+ "Confusion matrix count objek diperbarui: "
764
+ f"Person={delta['person']}, Not Person/Background={delta['not_person']}."
765
+ ),
766
  )
767
 
768
 
769
  def update_evaluation_auto(
770
+ cm_state: Dict[str, int], eval_payload: Dict[str, Any]
771
  ) -> Tuple[Dict[str, int], np.ndarray, str, str]:
772
+ if not eval_payload:
773
  state = dict(cm_state or CM_INIT)
774
  return (
775
  state,
 
777
  render_metric_markdown(state),
778
  "Jalankan deteksi terlebih dahulu agar evaluasi otomatis dapat diperbarui.",
779
  )
780
+ return update_evaluation(cm_state, eval_payload)
 
 
781
 
782
 
783
  def reset_evaluation() -> Tuple[Dict[str, int], np.ndarray, str, str]:
 
1044
  return str(output_path)
1045
 
1046
 
1047
+ def load_rgb_image(image_path: Path) -> np.ndarray | None:
1048
+ if not image_path.exists() or not image_path.is_file():
1049
+ return None
1050
+ bgr = cv2.imread(str(image_path))
1051
+ if bgr is None:
1052
+ return None
1053
+ return cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
1054
+
1055
+
1056
  def save_training_summary(paths: Dict[str, Path], state: Dict[str, Any], rows_count: int) -> str:
1057
  payload = {
1058
  "saved_at": datetime.now().isoformat(),
 
1069
  return str(paths["summary"])
1070
 
1071
 
1072
+ def sync_training_artifacts_to_hf(
1073
+ run_name: str,
1074
+ results_csv: Path | None,
1075
+ log_file: Path | None,
1076
+ chart_file: Path | None,
1077
+ summary_file: Path | None,
1078
+ ) -> str:
1079
+ hf_repo = os.getenv("HF_MODEL_REPO", "").strip()
1080
+ hf_repo_type = os.getenv("HF_MODEL_REPO_TYPE", "space").strip() or "space"
1081
+ hf_token = os.getenv("HF_TOKEN", "").strip() or None
1082
+ repo_dir = os.getenv("HF_TRAINING_DIR", "space_training_archive").strip().strip("/")
1083
+ run_name = run_name or datetime.now().strftime("training_%Y%m%d_%H%M%S")
1084
+
1085
+ if not hf_repo:
1086
+ return "HF training artifact sync dilewati: set HF_MODEL_REPO."
1087
+
1088
+ upload_items: list[tuple[Path | None, str]] = [
1089
+ (results_csv, f"{repo_dir}/{run_name}/results.csv"),
1090
+ (log_file, f"{repo_dir}/{run_name}/training.log"),
1091
+ (chart_file, f"{repo_dir}/{run_name}/training_curve.png"),
1092
+ (summary_file, f"{repo_dir}/{run_name}/training_summary.json"),
1093
+ (chart_file, "training_outputs/training_curve_latest.png"),
1094
+ (summary_file, "training_outputs/training_summary_latest.json"),
1095
+ ]
1096
+ try:
1097
+ api = HfApi(token=hf_token)
1098
+ uploaded = 0
1099
+ for local_path, repo_path in upload_items:
1100
+ if local_path is None or (not local_path.exists()) or (not local_path.is_file()):
1101
+ continue
1102
+ api.upload_file(
1103
+ path_or_fileobj=str(local_path),
1104
+ path_in_repo=repo_path,
1105
+ repo_id=hf_repo,
1106
+ repo_type=hf_repo_type,
1107
+ commit_message=f"Save training artifact {run_name}",
1108
+ )
1109
+ uploaded += 1
1110
+ return f"HF training artifact sync sukses: {uploaded} file -> {repo_dir}/{run_name}"
1111
+ except Exception as err:
1112
+ return f"HF training artifact sync gagal: {err}"
1113
+
1114
+
1115
  def _env_true(var_name: str, default: bool = False) -> bool:
1116
  val = os.getenv(var_name)
1117
  if val is None:
 
1421
  f"Tahap: **{stage}** \nEpoch tercatat: **{rows_count}**"
1422
  )
1423
  if status == "done":
1424
+ sync_msg = state.get("artifacts_sync_msg")
1425
+ suffix = f" \nArtefak: `{sync_msg}`" if sync_msg else ""
1426
+ return f"Status training: **Selesai** \nRun: `{run_name}`{suffix}"
1427
  if status == "failed":
1428
  err = state.get("error", "-")
1429
+ sync_msg = state.get("artifacts_sync_msg")
1430
+ suffix = f" \nArtefak: `{sync_msg}`" if sync_msg else ""
1431
+ return f"Status training: **Gagal** \nRun: `{run_name}` \nError: `{err}`{suffix}"
1432
  return f"Status training: **{status}** \nRun: `{run_name}`"
1433
 
1434
 
 
1441
 
1442
  results_csv = _safe_results_csv_path(state)
1443
  rows = _read_results_rows(results_csv) if results_csv is not None else []
1444
+ saved_chart = load_rgb_image(paths["chart"])
1445
+ chart = (
1446
+ plot_training_curves(results_csv)
1447
+ if results_csv is not None
1448
+ else saved_chart
1449
+ if saved_chart is not None
1450
+ else _make_info_image("Grafik training belum tersedia.")
1451
+ )
1452
+ chart_file = save_training_curve_image(chart, paths["chart"]) if results_csv is not None else (
1453
+ str(paths["chart"]) if paths["chart"].exists() else None
1454
  )
 
1455
 
1456
  if state.get("status") == "running":
1457
  pid = int(state.get("pid", 0) or 0)
 
1466
  metrics_file = str(results_csv) if results_csv is not None else None
1467
  log_file = str(paths["log"]) if paths["log"].exists() else None
1468
  summary_file = save_training_summary(paths, state, len(rows))
1469
+ if state.get("status") in {"done", "failed"} and not state.get("artifacts_sync_attempted"):
1470
+ sync_msg = sync_training_artifacts_to_hf(
1471
+ run_name=str(state.get("run_name", "training_run")),
1472
+ results_csv=results_csv,
1473
+ log_file=Path(log_file) if log_file else None,
1474
+ chart_file=Path(chart_file) if chart_file else None,
1475
+ summary_file=Path(summary_file) if summary_file else None,
1476
+ )
1477
+ state["artifacts_sync_attempted"] = True
1478
+ state["artifacts_sync_msg"] = sync_msg
1479
+ _write_status(paths["status"], state)
1480
  return (
1481
  log_text,
1482
  best_file,
 
1499
 
1500
 
1501
  def run_training(
1502
+ train_model_choice: str,
1503
  epochs: float,
1504
  imgsz: float,
1505
  batch: float,
 
1537
  log_file=paths["log"],
1538
  )
1539
 
1540
+ if train_model_choice == "Best.pt":
1541
+ model_path = resolve_model_path("Best.pt")
1542
+ else:
1543
+ model_path = str(base_dir / "yolov8n.pt") if (base_dir / "yolov8n.pt").exists() else resolve_model_path("YOLOv8n")
1544
  run_name = f"classroom_train_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
1545
  project_dir = base_dir / "runs_train"
1546
  save_dir = project_dir / run_name
 
1548
 
1549
  with paths["log"].open("a", encoding="utf-8") as lf:
1550
  lf.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Start training {run_name}\n")
1551
+ lf.write(f"Model awal training: {train_model_choice} ({model_path})\n")
1552
  lf.write(f"{prep_info}\n")
1553
 
1554
  train_code = r"""
 
1696
  }
1697
  .version-foot {font-size:12px; color:#64748b; text-align:center; margin-top:8px;}
1698
  .advanced-box {margin-top: 8px; padding: 8px; border: 1px dashed #cbd5e1; border-radius: 8px;}
1699
+ .section-box table {width: 100%; border-collapse: collapse; font-size: 14px;}
1700
+ .section-box th, .section-box td {padding: 8px 10px; border-bottom: 1px solid #e2e8f0; vertical-align: top;}
1701
+ .section-box th {background: #f8fafc; font-weight: 700;}
1702
 
1703
  @media (prefers-color-scheme: dark) {
1704
  body, .gradio-container {background: #0f172a !important; color: #e2e8f0 !important;}
 
1708
  .version-chip {background: #111827 !important; border-color: #475569 !important; color: #cbd5e1 !important;}
1709
  .version-foot {color: #94a3b8 !important;}
1710
  .advanced-box {border-color: #475569 !important;}
1711
+ .section-box th {background: #1f2937 !important;}
1712
+ .section-box th, .section-box td {border-color: #334155 !important;}
1713
  }
1714
  """
1715
 
 
1743
  )
1744
 
1745
  with gr.Column(elem_classes=["section-box"]):
1746
+ gr.Markdown("## Tahap 1 - Pilih Model", elem_classes=["section-title"])
1747
  inference_mode = gr.Dropdown(
1748
  choices=["Single Model", "Bandingkan (YOLOv8n vs Best.pt)"],
1749
  value="Single Model",
 
1754
  value="YOLOv8n",
1755
  label="Pilih Model",
1756
  )
 
 
 
 
 
1757
  conf_input = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Parameter Confidence (Deteksi)")
1758
  with gr.Accordion("Advanced Options", open=False, elem_classes=["advanced-box"]):
1759
  annotation_style = gr.Radio(
 
1762
  label="Gaya anotasi objek",
1763
  info="Pilih tampilan bbox atau label-only untuk label objek yang terdeteksi.",
1764
  )
 
1765
 
1766
  with gr.Column(elem_classes=["section-box"]):
1767
+ gr.Markdown("## Tahap 2 - Input Gambar", elem_classes=["section-title"])
1768
+ image_input = gr.Image(
1769
+ label="Input Gambar Kelas (Upload/Kamera)",
1770
+ type="numpy",
1771
+ sources=["upload", "webcam"],
 
 
 
1772
  )
1773
+ run_btn = gr.Button("Jalankan Deteksi dan Klasifikasi", variant="primary")
 
 
 
 
1774
 
1775
  with gr.Column(elem_classes=["section-box"]):
1776
+ gr.Markdown("## Tahap 3 - Grafik dan Training", elem_classes=["section-title"])
1777
+ comparison_chart = gr.Image(
1778
+ label="Grafik Count Person dan Not Person",
1779
+ type="numpy",
1780
+ value=_make_info_image("Grafik hasil deteksi akan tampil setelah proses inferensi."),
 
 
 
1781
  )
1782
+ with gr.Accordion("Menu Training Model", open=False):
 
 
 
 
 
 
 
 
 
 
 
 
 
1783
  gr.Markdown(
1784
  "Training akan membaca dataset dari `dataset/train` dan `dataset/test`.\n"
1785
  "Jika label `.txt` belum ada, aktifkan pseudo-label agar proses tetap jalan.\n"
1786
  "Saat training berjalan, terminal progres dan grafik epoch akan update otomatis."
1787
  )
1788
+ train_model_choice = gr.Dropdown(
1789
+ choices=["YOLOv8n", "Best.pt"],
1790
+ value="YOLOv8n",
1791
+ label="Model awal training",
1792
+ info="Pilih YOLOv8n untuk training awal, atau Best.pt untuk melanjutkan fine-tuning dari bobot terakhir.",
1793
+ )
1794
  with gr.Row():
1795
  train_epochs = gr.Number(label="Epochs", value=10, precision=0, minimum=1)
1796
  train_imgsz = gr.Number(label="Image Size", value=640, precision=0, minimum=64)
 
1820
  training_log_file = gr.File(label="training.log / Log Training Lokal")
1821
  training_chart_file = gr.File(label="training_curve_latest.png / Grafik Training Lokal")
1822
 
1823
+ with gr.Column(elem_classes=["section-box"]):
1824
+ gr.Markdown("## Tahap 4 - Evaluasi (Confusion Matrix + Rumus)", elem_classes=["section-title"])
1825
+ cm_state = gr.State(dict(CM_INIT))
1826
+ predicted_state = gr.State("")
1827
+ actual_state = gr.State("")
1828
+ eval_payload_state = gr.State({})
1829
+ gr.Markdown(
1830
+ "Evaluasi otomatis memakai count objek. `Person` dihitung sebagai objek positif, "
1831
+ "sedangkan objek selain person dihitung sebagai `Not Person/Background`."
1832
+ )
1833
+ reset_eval_btn = gr.Button("Reset Evaluasi")
1834
+ eval_status = gr.Markdown("Belum ada update evaluasi otomatis.")
1835
+ with gr.Row():
1836
+ cm_image = gr.Image(
1837
+ label="Confusion Matrix Count Objek",
1838
+ value=draw_confusion_matrix_from_state(CM_INIT),
1839
+ type="numpy",
1840
+ interactive=False,
1841
+ )
1842
+ metric_md = gr.Markdown(render_metric_markdown(CM_INIT))
1843
+
1844
+ with gr.Column(elem_classes=["section-box"]):
1845
+ gr.Markdown("## Tahap 5 - Hasil Gambar", elem_classes=["section-title"])
1846
+ output_image_yolo = gr.Image(
1847
+ label="Output Deteksi YOLOv8n",
1848
+ type="filepath",
1849
+ )
1850
+ output_image_best = gr.Image(
1851
+ label="Output Deteksi Best.pt",
1852
+ type="filepath",
1853
+ )
1854
+ class_result_md = gr.Markdown("Ringkasan hasil deteksi akan tampil di sini.")
1855
+ predicted_label_md = gr.Markdown("Label prediksi evaluasi: **-**")
1856
+ saved_detection_file = gr.File(label="File Gambar Hasil Evaluasi")
1857
+ evaluation_csv_file = gr.File(label="Log CSV Evaluasi Lokal")
1858
+
1859
  run_btn_evt = run_btn.click(
1860
  fn=run_check_and_classify,
1861
  inputs=[image_input, conf_input, model_choice, inference_mode, annotation_style],
 
1866
  comparison_chart,
1867
  predicted_state,
1868
  actual_state,
1869
+ eval_payload_state,
1870
  predicted_label_md,
1871
  saved_detection_file,
1872
  evaluation_csv_file,
 
1874
  )
1875
  run_btn_evt.then(
1876
  fn=update_evaluation_auto,
1877
+ inputs=[cm_state, eval_payload_state],
1878
  outputs=[cm_state, cm_image, metric_md, eval_status],
1879
  )
1880
  inference_mode.change(
 
1891
 
1892
  train_btn.click(
1893
  fn=run_training,
1894
+ inputs=[train_model_choice, train_epochs, train_imgsz, train_batch, use_pseudo, pseudo_conf],
1895
  outputs=[
1896
  train_log,
1897
  best_model_file,