firgiawann commited on
Commit
633942d
·
verified ·
1 Parent(s): 0386731

Add advanced annotation style options and automatic dark mode UI support (update 17)

Browse files
Files changed (3) hide show
  1. CHANGELOG.md +6 -0
  2. README.md +3 -0
  3. app.py +59 -16
CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
  # Changelog
2
 
 
 
 
 
 
 
3
  ## Update ke-16 - 2026-04-15
4
  - Perbaikan error Gradio `IsADirectoryError: '/app'` pada output file training.
5
  - Output komponen `gr.File` di mode streaming kini mengirim `None` (bukan string kosong) saat file belum siap.
 
1
  # Changelog
2
 
3
+ ## Update ke-17 - 2026-04-15
4
+ - Tambah `Advanced Options` pada inferensi: pilih gaya anotasi `Border + Label` atau `Label saja`.
5
+ - Opsi anotasi berlaku untuk semua model (YOLOv8n, Best.pt, dan mode perbandingan).
6
+ - Tambah adaptasi UI dark mode otomatis via `prefers-color-scheme: dark`.
7
+ - Minor update dinaikkan ke-17.
8
+
9
  ## Update ke-16 - 2026-04-15
10
  - Perbaikan error Gradio `IsADirectoryError: '/app'` pada output file training.
11
  - Output komponen `gr.File` di mode streaming kini mengirim `None` (bukan string kosong) saat file belum siap.
README.md CHANGED
@@ -36,6 +36,8 @@ Versi ini difokuskan untuk tahap awal:
36
  - **Section 1 - Input & Proses**
37
  - input gambar (upload/webcam)
38
  - confidence threshold
 
 
39
  - tombol proses
40
  - **Section 2 - Hasil Processing**
41
  - hasil gambar deteksi
@@ -53,6 +55,7 @@ Catatan:
53
  - input evaluasi batch dihapus.
54
  - training tersedia sebagai menu terpisah (accordion) di halaman yang sama.
55
  - input jumlah presensi belum digunakan pada versi ini.
 
56
 
57
  ## File Penting
58
 
 
36
  - **Section 1 - Input & Proses**
37
  - input gambar (upload/webcam)
38
  - confidence threshold
39
+ - advanced options:
40
+ - gaya anotasi `Border + Label` atau `Label saja`
41
  - tombol proses
42
  - **Section 2 - Hasil Processing**
43
  - hasil gambar deteksi
 
55
  - input evaluasi batch dihapus.
56
  - training tersedia sebagai menu terpisah (accordion) di halaman yang sama.
57
  - input jumlah presensi belum digunakan pada versi ini.
58
+ - UI otomatis menyesuaikan mode gelap perangkat (dark mode).
59
 
60
  ## File Penting
61
 
app.py CHANGED
@@ -24,7 +24,7 @@ from ultralytics import YOLO
24
  CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
25
  EVAL_CLASSES = ["Sesuai", "Tidak Sesuai"]
26
  APP_VERSION = os.getenv("APP_VERSION", "v0.9.0")
27
- APP_MINOR_UPDATE = os.getenv("APP_MINOR_UPDATE", "16")
28
 
29
 
30
  # Gradio/Spaces can trigger noisy asyncio cleanup traces ("Invalid file descriptor: -1")
@@ -108,7 +108,12 @@ def _resolve_person_class_id_from_names(names: Any) -> int:
108
  return 0
109
 
110
 
111
- def _annotate_person_instances(image_rgb: np.ndarray, result: Any, person_id: int) -> Tuple[np.ndarray, int]:
 
 
 
 
 
112
  annotated = image_rgb.copy()
113
  if result.boxes is None:
114
  return annotated, 0
@@ -128,17 +133,29 @@ def _annotate_person_instances(image_rgb: np.ndarray, result: Any, person_id: in
128
  person_boxes.sort(key=lambda b: (b[1], b[0]))
129
 
130
  for idx, (x1, y1, x2, y2) in enumerate(person_boxes, start=1):
131
- cv2.rectangle(annotated, (x1, y1), (x2, y2), (34, 197, 94), 2)
 
132
  label = f"person {idx}"
133
  (tw, th), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
134
- text_y1 = max(0, y1 - th - baseline - 6)
135
- text_y2 = y1
136
- text_x2 = min(annotated.shape[1], x1 + tw + 10)
137
- cv2.rectangle(annotated, (x1, text_y1), (text_x2, text_y2), (34, 197, 94), -1)
 
 
 
 
 
 
 
 
 
 
 
138
  cv2.putText(
139
  annotated,
140
  label,
141
- (x1 + 4, max(14, y1 - 6)),
142
  cv2.FONT_HERSHEY_SIMPLEX,
143
  0.6,
144
  (0, 0, 0),
@@ -148,7 +165,12 @@ def _annotate_person_instances(image_rgb: np.ndarray, result: Any, person_id: in
148
  return annotated, len(person_boxes)
149
 
150
 
151
- def infer_image(image: np.ndarray, conf_threshold: float, model_choice: str) -> Tuple[np.ndarray, int]:
 
 
 
 
 
152
  if image is None:
153
  raise ValueError("Silakan unggah gambar atau gunakan kamera.")
154
  model = load_model(model_choice)
@@ -161,7 +183,7 @@ def infer_image(image: np.ndarray, conf_threshold: float, model_choice: str) ->
161
  verbose=False,
162
  )[0]
163
  person_id = _resolve_person_class_id(result)
164
- annotated, detected_count = _annotate_person_instances(image, result, person_id)
165
  return annotated, detected_count
166
 
167
 
@@ -242,12 +264,13 @@ def run_check_and_classify(
242
  attendance_count: float,
243
  model_choice: str,
244
  inference_mode: str,
 
245
  ) -> Tuple[np.ndarray, str, np.ndarray, str, str]:
246
  try:
247
  attendance = int(attendance_count)
248
  if inference_mode == "Bandingkan (YOLOv8n vs Best.pt)":
249
- ann_before, det_before = infer_image(image, conf_threshold, "YOLOv8n")
250
- ann_after, det_after = infer_image(image, conf_threshold, "Best.pt")
251
  match_before = classify_match(det_before, attendance)
252
  match_after = classify_match(det_after, attendance)
253
  presence_before = classify_presence(det_before)
@@ -264,7 +287,7 @@ def run_check_and_classify(
264
  pred_md = f"Label prediksi validasi (Best.pt): **{match_after}**"
265
  return combined, summary, chart, match_after, pred_md
266
 
267
- annotated, detected = infer_image(image, conf_threshold, model_choice)
268
  status_presence = classify_presence(detected)
269
  status_match = classify_match(detected, attendance)
270
  summary = (
@@ -274,7 +297,8 @@ def run_check_and_classify(
274
  f"- Klasifikasi keberadaan: **{status_presence}**\n"
275
  f"- Validasi presensi: **{status_match}**\n"
276
  f"- Model digunakan: **{model_choice}**\n"
277
- "- Label bounding box: **person 1, person 2, ...**"
 
278
  )
279
  chart = draw_detection_chart(detected)
280
  pred_md = f"Label prediksi validasi: **{status_match}**"
@@ -997,11 +1021,23 @@ CUSTOM_CSS = """
997
  .section-box {padding: 14px; border: 1px solid #d7dee8; border-radius: 10px; background: #ffffff;}
998
  .method-box {padding: 14px; border: 1px solid #c7d2e3; border-radius: 10px; background: #f8fbff;}
999
  .section-title {margin-bottom: 4px !important;}
 
1000
  .version-chip {
1001
  display:inline-block; padding:4px 10px; border:1px solid #c7d2e3; border-radius:999px;
1002
  font-size:12px; color:#334155; background:#f8fbff; margin-top:4px;
1003
  }
1004
  .version-foot {font-size:12px; color:#64748b; text-align:center; margin-top:8px;}
 
 
 
 
 
 
 
 
 
 
 
1005
  """
1006
 
1007
 
@@ -1009,7 +1045,7 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1009
  gr.HTML(
1010
  """
1011
  <h1 style="text-align:center;margin-bottom:4px;">Pengecekan dan Klasifikasi Kehadiran Berbasis YOLOv8</h1>
1012
- <p style="text-align:center;color:#4a5568;margin-top:0;">
1013
  Implementasi klasifikasi kehadiran mahasiswa dalam ruang kelas berbasis citra menggunakan YOLOv8.
1014
  </p>
1015
  """
@@ -1057,6 +1093,13 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1057
  minimum=0,
1058
  )
1059
  conf_input = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Parameter Confidence (Deteksi)")
 
 
 
 
 
 
 
1060
  run_btn = gr.Button("Jalankan Deteksi dan Klasifikasi", variant="primary")
1061
 
1062
  with gr.Column(elem_classes=["section-box"]):
@@ -1119,7 +1162,7 @@ with gr.Blocks(title="Pengecekan dan Klasifikasi Mahasiswa - YOLOv8") as demo:
1119
 
1120
  run_btn_evt = run_btn.click(
1121
  fn=run_check_and_classify,
1122
- inputs=[image_input, conf_input, attendance_input, model_choice, inference_mode],
1123
  outputs=[output_image, class_result_md, comparison_chart, predicted_state, predicted_label_md],
1124
  )
1125
  run_btn_evt.then(
 
24
  CM_INIT = {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
25
  EVAL_CLASSES = ["Sesuai", "Tidak Sesuai"]
26
  APP_VERSION = os.getenv("APP_VERSION", "v0.9.0")
27
+ APP_MINOR_UPDATE = os.getenv("APP_MINOR_UPDATE", "17")
28
 
29
 
30
  # Gradio/Spaces can trigger noisy asyncio cleanup traces ("Invalid file descriptor: -1")
 
108
  return 0
109
 
110
 
111
+ def _annotate_person_instances(
112
+ image_rgb: np.ndarray,
113
+ result: Any,
114
+ person_id: int,
115
+ annotation_style: str,
116
+ ) -> Tuple[np.ndarray, int]:
117
  annotated = image_rgb.copy()
118
  if result.boxes is None:
119
  return annotated, 0
 
133
  person_boxes.sort(key=lambda b: (b[1], b[0]))
134
 
135
  for idx, (x1, y1, x2, y2) in enumerate(person_boxes, start=1):
136
+ if annotation_style == "Border + Label":
137
+ cv2.rectangle(annotated, (x1, y1), (x2, y2), (34, 197, 94), 2)
138
  label = f"person {idx}"
139
  (tw, th), baseline = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)
140
+ if annotation_style == "Label saja":
141
+ label_x = max(0, min(x1, annotated.shape[1] - tw - 8))
142
+ label_y = max(th + 4, min(y1 + th + 4, annotated.shape[0] - 6))
143
+ text_y1 = max(0, label_y - th - baseline - 6)
144
+ text_y2 = min(annotated.shape[0], label_y + 2)
145
+ text_x1 = label_x
146
+ text_x2 = min(annotated.shape[1], label_x + tw + 10)
147
+ text_org = (label_x + 4, label_y - 4)
148
+ else:
149
+ text_y1 = max(0, y1 - th - baseline - 6)
150
+ text_y2 = y1
151
+ text_x1 = x1
152
+ text_x2 = min(annotated.shape[1], x1 + tw + 10)
153
+ text_org = (x1 + 4, max(14, y1 - 6))
154
+ cv2.rectangle(annotated, (text_x1, text_y1), (text_x2, text_y2), (34, 197, 94), -1)
155
  cv2.putText(
156
  annotated,
157
  label,
158
+ text_org,
159
  cv2.FONT_HERSHEY_SIMPLEX,
160
  0.6,
161
  (0, 0, 0),
 
165
  return annotated, len(person_boxes)
166
 
167
 
168
+ def infer_image(
169
+ image: np.ndarray,
170
+ conf_threshold: float,
171
+ model_choice: str,
172
+ annotation_style: str,
173
+ ) -> Tuple[np.ndarray, int]:
174
  if image is None:
175
  raise ValueError("Silakan unggah gambar atau gunakan kamera.")
176
  model = load_model(model_choice)
 
183
  verbose=False,
184
  )[0]
185
  person_id = _resolve_person_class_id(result)
186
+ annotated, detected_count = _annotate_person_instances(image, result, person_id, annotation_style)
187
  return annotated, detected_count
188
 
189
 
 
264
  attendance_count: float,
265
  model_choice: str,
266
  inference_mode: str,
267
+ annotation_style: str,
268
  ) -> Tuple[np.ndarray, str, np.ndarray, str, str]:
269
  try:
270
  attendance = int(attendance_count)
271
  if inference_mode == "Bandingkan (YOLOv8n vs Best.pt)":
272
+ ann_before, det_before = infer_image(image, conf_threshold, "YOLOv8n", annotation_style)
273
+ ann_after, det_after = infer_image(image, conf_threshold, "Best.pt", annotation_style)
274
  match_before = classify_match(det_before, attendance)
275
  match_after = classify_match(det_after, attendance)
276
  presence_before = classify_presence(det_before)
 
287
  pred_md = f"Label prediksi validasi (Best.pt): **{match_after}**"
288
  return combined, summary, chart, match_after, pred_md
289
 
290
+ annotated, detected = infer_image(image, conf_threshold, model_choice, annotation_style)
291
  status_presence = classify_presence(detected)
292
  status_match = classify_match(detected, attendance)
293
  summary = (
 
297
  f"- Klasifikasi keberadaan: **{status_presence}**\n"
298
  f"- Validasi presensi: **{status_match}**\n"
299
  f"- Model digunakan: **{model_choice}**\n"
300
+ f"- Gaya anotasi: **{annotation_style}**\n"
301
+ "- Label objek: **person 1, person 2, ...**"
302
  )
303
  chart = draw_detection_chart(detected)
304
  pred_md = f"Label prediksi validasi: **{status_match}**"
 
1021
  .section-box {padding: 14px; border: 1px solid #d7dee8; border-radius: 10px; background: #ffffff;}
1022
  .method-box {padding: 14px; border: 1px solid #c7d2e3; border-radius: 10px; background: #f8fbff;}
1023
  .section-title {margin-bottom: 4px !important;}
1024
+ .hero-subtitle {text-align:center;color:#4a5568;margin-top:0;}
1025
  .version-chip {
1026
  display:inline-block; padding:4px 10px; border:1px solid #c7d2e3; border-radius:999px;
1027
  font-size:12px; color:#334155; background:#f8fbff; margin-top:4px;
1028
  }
1029
  .version-foot {font-size:12px; color:#64748b; text-align:center; margin-top:8px;}
1030
+ .advanced-box {margin-top: 8px; padding: 8px; border: 1px dashed #cbd5e1; border-radius: 8px;}
1031
+
1032
+ @media (prefers-color-scheme: dark) {
1033
+ body, .gradio-container {background: #0f172a !important; color: #e2e8f0 !important;}
1034
+ .section-box {background: #111827 !important; border-color: #334155 !important;}
1035
+ .method-box {background: #0b1220 !important; border-color: #334155 !important;}
1036
+ .hero-subtitle {color: #94a3b8 !important;}
1037
+ .version-chip {background: #111827 !important; border-color: #475569 !important; color: #cbd5e1 !important;}
1038
+ .version-foot {color: #94a3b8 !important;}
1039
+ .advanced-box {border-color: #475569 !important;}
1040
+ }
1041
  """
1042
 
1043
 
 
1045
  gr.HTML(
1046
  """
1047
  <h1 style="text-align:center;margin-bottom:4px;">Pengecekan dan Klasifikasi Kehadiran Berbasis YOLOv8</h1>
1048
+ <p class="hero-subtitle">
1049
  Implementasi klasifikasi kehadiran mahasiswa dalam ruang kelas berbasis citra menggunakan YOLOv8.
1050
  </p>
1051
  """
 
1093
  minimum=0,
1094
  )
1095
  conf_input = gr.Slider(0.1, 1.0, value=0.25, step=0.05, label="Parameter Confidence (Deteksi)")
1096
+ with gr.Accordion("Advanced Options", open=False, elem_classes=["advanced-box"]):
1097
+ annotation_style = gr.Radio(
1098
+ choices=["Border + Label", "Label saja"],
1099
+ value="Border + Label",
1100
+ label="Gaya anotasi objek",
1101
+ info="Pilih tampilan bbox atau label-only untuk person 1, person 2, ...",
1102
+ )
1103
  run_btn = gr.Button("Jalankan Deteksi dan Klasifikasi", variant="primary")
1104
 
1105
  with gr.Column(elem_classes=["section-box"]):
 
1162
 
1163
  run_btn_evt = run_btn.click(
1164
  fn=run_check_and_classify,
1165
+ inputs=[image_input, conf_input, attendance_input, model_choice, inference_mode, annotation_style],
1166
  outputs=[output_image, class_result_md, comparison_chart, predicted_state, predicted_label_md],
1167
  )
1168
  run_btn_evt.then(