Revise layout evaluation and training artifact sync update 32
Browse files
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", "
|
| 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(
|
| 378 |
-
fig, ax = plt.subplots(figsize=(
|
| 379 |
-
labels = ["
|
| 380 |
-
values = [
|
| 381 |
-
bars = ax.bar(labels, values, color=["#
|
| 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(
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
ax.
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 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=(
|
| 606 |
sns.heatmap(
|
| 607 |
cm,
|
| 608 |
annot=True,
|
| 609 |
fmt="d",
|
| 610 |
cmap="Blues",
|
| 611 |
cbar=True,
|
| 612 |
-
xticklabels=
|
| 613 |
-
yticklabels=
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 645 |
-
"
|
| 646 |
-
f"
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
f"
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 666 |
-
|
| 667 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 679 |
)
|
| 680 |
|
| 681 |
|
| 682 |
def update_evaluation_auto(
|
| 683 |
-
cm_state: Dict[str, int],
|
| 684 |
) -> Tuple[Dict[str, int], np.ndarray, str, str]:
|
| 685 |
-
if
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 1288 |
if status == "failed":
|
| 1289 |
err = state.get("error", "-")
|
| 1290 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 1304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 -
|
| 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 -
|
| 1605 |
-
|
| 1606 |
-
label="
|
| 1607 |
-
type="
|
| 1608 |
-
|
| 1609 |
-
output_image_best = gr.Image(
|
| 1610 |
-
label="Output Deteksi Best.pt",
|
| 1611 |
-
type="filepath",
|
| 1612 |
)
|
| 1613 |
-
|
| 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 -
|
| 1621 |
-
|
| 1622 |
-
|
| 1623 |
-
|
| 1624 |
-
|
| 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.
|
| 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,
|
| 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,
|