Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Đếm xe qua line - Deploy lên Hugging Face Spaces với Gradio
|
| 3 |
-
"""
|
| 4 |
import os
|
| 5 |
import tempfile
|
| 6 |
import cv2
|
|
@@ -9,21 +6,18 @@ import gradio as gr
|
|
| 9 |
import supervision as sv
|
| 10 |
from ultralytics import YOLO
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
DEFAULT_DETECT_EVERY_N_FRAMES = 2 # Chỉ detect mỗi N frame để giảm tải CPU
|
| 15 |
|
| 16 |
-
# Load model (yolov8n nhẹ cho CPU miễn phí)
|
| 17 |
model = YOLO("yolov8n.pt")
|
| 18 |
CLASS_NAMES_DICT = model.model.names
|
| 19 |
|
| 20 |
-
SELECTED_CLASS_NAMES = ['
|
| 21 |
SELECTED_CLASS_IDS = [
|
| 22 |
{value: key for key, value in CLASS_NAMES_DICT.items()}[name]
|
| 23 |
for name in SELECTED_CLASS_NAMES
|
| 24 |
]
|
| 25 |
|
| 26 |
-
# Annotators (tạo 1 lần, dùng lại)
|
| 27 |
box_annotator = sv.BoxAnnotator(thickness=4)
|
| 28 |
label_annotator = sv.LabelAnnotator(text_thickness=2, text_scale=1.5, text_color=sv.Color.BLACK)
|
| 29 |
trace_annotator = sv.TraceAnnotator(thickness=4, trace_length=50)
|
|
@@ -32,24 +26,14 @@ def process_video(
|
|
| 32 |
use_resize: bool = True,
|
| 33 |
max_frame_size: int = DEFAULT_MAX_FRAME_SIZE,
|
| 34 |
detect_every_n: int = DEFAULT_DETECT_EVERY_N_FRAMES,
|
| 35 |
-
line_orientation: str = "Ngang",
|
| 36 |
):
|
| 37 |
-
"""Xử lý video: đếm xe qua line và trả về video đã annotate.
|
| 38 |
-
|
| 39 |
-
Các tham số tối ưu và cấu hình được truyền từ UI:
|
| 40 |
-
- use_resize: có resize khung hình trước khi detect hay không
|
| 41 |
-
- max_frame_size: cạnh dài tối đa sau resize
|
| 42 |
-
- detect_every_n: chỉ detect mỗi N frame (1 = detect mọi frame)
|
| 43 |
-
- line_orientation: "Ngang" hoặc "Dọc"
|
| 44 |
-
"""
|
| 45 |
if video_path is None:
|
| 46 |
return None
|
| 47 |
|
| 48 |
-
# Gradio Video có thể trả về dict với key "path"
|
| 49 |
if isinstance(video_path, dict):
|
| 50 |
video_path = video_path.get("path", video_path)
|
| 51 |
|
| 52 |
-
# Lấy thông tin video (dùng cho tracker)
|
| 53 |
video_info = sv.VideoInfo.from_video_path(video_path)
|
| 54 |
|
| 55 |
byte_tracker = sv.ByteTrack(
|
|
@@ -68,7 +52,6 @@ def process_video(
|
|
| 68 |
def callback(frame: np.ndarray, index: int) -> np.ndarray:
|
| 69 |
nonlocal previous_positions, class_counts, crossed_ids
|
| 70 |
|
| 71 |
-
# Đảm bảo giá trị hợp lệ
|
| 72 |
if max_frame_size is None or max_frame_size <= 0:
|
| 73 |
max_size = DEFAULT_MAX_FRAME_SIZE
|
| 74 |
else:
|
|
@@ -79,7 +62,6 @@ def process_video(
|
|
| 79 |
else:
|
| 80 |
detect_every = int(detect_every_n)
|
| 81 |
|
| 82 |
-
# Resize khung hình trước khi đưa vào YOLO để giảm tải (nếu bật)
|
| 83 |
fh_orig, fw_orig = frame.shape[:2]
|
| 84 |
if use_resize:
|
| 85 |
scale = min(1.0, max_size / max(fh_orig, fw_orig))
|
|
@@ -92,9 +74,7 @@ def process_video(
|
|
| 92 |
else:
|
| 93 |
frame_infer = frame
|
| 94 |
|
| 95 |
-
# Tính lại kích thước sau khi resize
|
| 96 |
fh, fw = frame_infer.shape[:2]
|
| 97 |
-
# Vị trí line theo hướng người dùng chọn
|
| 98 |
if line_orientation == "Dọc":
|
| 99 |
line_pos = int(fw * 0.5)
|
| 100 |
is_horizontal = False
|
|
@@ -102,11 +82,9 @@ def process_video(
|
|
| 102 |
line_pos = int(fh * 0.5)
|
| 103 |
is_horizontal = True
|
| 104 |
|
| 105 |
-
# Bỏ qua một số frame để giảm số lần detect
|
| 106 |
if detect_every > 1 and index % detect_every != 0:
|
| 107 |
annotator_frame = frame_infer.copy()
|
| 108 |
|
| 109 |
-
# Vẽ line đếm theo cấu hình
|
| 110 |
if is_horizontal:
|
| 111 |
cv2.line(
|
| 112 |
annotator_frame,
|
|
@@ -124,7 +102,6 @@ def process_video(
|
|
| 124 |
2,
|
| 125 |
)
|
| 126 |
|
| 127 |
-
# Khung tổng đếm (dùng class_counts hiện tại)
|
| 128 |
box_w, box_h = 280, 50 + len(SELECTED_CLASS_NAMES) * 28
|
| 129 |
x0, y0 = fw - box_w - 20, 20
|
| 130 |
|
|
@@ -161,7 +138,6 @@ def process_video(
|
|
| 161 |
detections = detections[np.isin(detections.class_id, SELECTED_CLASS_IDS)]
|
| 162 |
detections = byte_tracker.update_with_detections(detections)
|
| 163 |
|
| 164 |
-
# Đếm theo loại khi qua line
|
| 165 |
if detections.tracker_id is not None:
|
| 166 |
xyxy = detections.xyxy
|
| 167 |
for i in range(len(detections)):
|
|
@@ -171,7 +147,6 @@ def process_video(
|
|
| 171 |
cx = (xyxy[i, 0] + xyxy[i, 2]) / 2
|
| 172 |
cy = (xyxy[i, 1] + xyxy[i, 3]) / 2
|
| 173 |
|
| 174 |
-
# Toạ độ 1D dùng để kiểm tra qua line (y nếu line ngang, x nếu line dọc)
|
| 175 |
curr_coord = cy if is_horizontal else cx
|
| 176 |
|
| 177 |
if tid in previous_positions:
|
|
@@ -206,7 +181,6 @@ def process_video(
|
|
| 206 |
scene=annotator_frame, detections=detections, labels=labels
|
| 207 |
)
|
| 208 |
|
| 209 |
-
# Vẽ line đếm theo cấu hình
|
| 210 |
if is_horizontal:
|
| 211 |
cv2.line(
|
| 212 |
annotator_frame,
|
|
@@ -224,7 +198,6 @@ def process_video(
|
|
| 224 |
2,
|
| 225 |
)
|
| 226 |
|
| 227 |
-
# Khung tổng đếm
|
| 228 |
box_w, box_h = 280, 50 + len(SELECTED_CLASS_NAMES) * 28
|
| 229 |
x0, y0 = fw - box_w - 20, 20
|
| 230 |
|
|
@@ -265,17 +238,12 @@ def process_video(
|
|
| 265 |
return output_path
|
| 266 |
|
| 267 |
|
| 268 |
-
|
| 269 |
-
with gr.Blocks(title="Đếm xe qua line", theme=gr.themes.Soft(primary_hue="blue", secondary_hue="gray")) as demo:
|
| 270 |
-
# Header đẹp hơn + thông tin sinh viên
|
| 271 |
with gr.Row():
|
| 272 |
gr.Markdown(
|
| 273 |
"""
|
| 274 |
<div style="display:flex;flex-direction:column;gap:4px;">
|
| 275 |
-
<h1 style="margin-bottom:4px;">🚗
|
| 276 |
-
<p style="margin:0;font-size:16px;color:#4b5563;">
|
| 277 |
-
Dự án: Đếm đối tượng qua line (Horizonal / Vertical) dùng YOLOv8 + ByteTrack.
|
| 278 |
-
</p>
|
| 279 |
<p style="margin:0;font-size:14px;color:#6b7280;">
|
| 280 |
Thực hiện: <strong>Trần Hải Nam - 223332840</strong>
|
| 281 |
</p>
|
|
@@ -295,7 +263,7 @@ with gr.Blocks(title="Đếm xe qua line", theme=gr.themes.Soft(primary_hue="blu
|
|
| 295 |
with gr.Column(scale=1):
|
| 296 |
gr.Markdown(
|
| 297 |
"### ✅ Kết quả đã xử lý\n"
|
| 298 |
-
"Hiển thị
|
| 299 |
)
|
| 300 |
video_output = gr.Video(label="Video đã xử lý")
|
| 301 |
|
|
@@ -303,12 +271,12 @@ with gr.Blocks(title="Đếm xe qua line", theme=gr.themes.Soft(primary_hue="blu
|
|
| 303 |
with gr.Accordion("⚙️ Tùy chọn nâng cao", open=False):
|
| 304 |
with gr.Row():
|
| 305 |
use_resize = gr.Checkbox(
|
| 306 |
-
value=True, label="Giảm kích thước khung hình trước khi
|
| 307 |
)
|
| 308 |
line_orientation = gr.Radio(
|
| 309 |
choices=["Ngang", "Dọc"],
|
| 310 |
value="Ngang",
|
| 311 |
-
label="Hướng
|
| 312 |
)
|
| 313 |
with gr.Row():
|
| 314 |
max_frame_size = gr.Slider(
|
|
@@ -337,12 +305,12 @@ with gr.Blocks(title="Đếm xe qua line", theme=gr.themes.Soft(primary_hue="blu
|
|
| 337 |
"""
|
| 338 |
---
|
| 339 |
### ℹ️ Gợi ý sử dụng
|
| 340 |
-
- Mặc định
|
| 341 |
-
- Có thể chuyển sang
|
| 342 |
-
-
|
| 343 |
- Dùng video **ngắn** (< 30 giây).
|
| 344 |
- Tăng `Detect mỗi N frame` nếu muốn xử lý nhanh hơn.
|
| 345 |
-
- Model sử dụng: **YOLOv8n**
|
| 346 |
"""
|
| 347 |
)
|
| 348 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import tempfile
|
| 3 |
import cv2
|
|
|
|
| 6 |
import supervision as sv
|
| 7 |
from ultralytics import YOLO
|
| 8 |
|
| 9 |
+
DEFAULT_MAX_FRAME_SIZE = 640
|
| 10 |
+
DEFAULT_DETECT_EVERY_N_FRAMES = 2
|
|
|
|
| 11 |
|
|
|
|
| 12 |
model = YOLO("yolov8n.pt")
|
| 13 |
CLASS_NAMES_DICT = model.model.names
|
| 14 |
|
| 15 |
+
SELECTED_CLASS_NAMES = ['car', 'truck', 'bus', 'motorcycle', ]
|
| 16 |
SELECTED_CLASS_IDS = [
|
| 17 |
{value: key for key, value in CLASS_NAMES_DICT.items()}[name]
|
| 18 |
for name in SELECTED_CLASS_NAMES
|
| 19 |
]
|
| 20 |
|
|
|
|
| 21 |
box_annotator = sv.BoxAnnotator(thickness=4)
|
| 22 |
label_annotator = sv.LabelAnnotator(text_thickness=2, text_scale=1.5, text_color=sv.Color.BLACK)
|
| 23 |
trace_annotator = sv.TraceAnnotator(thickness=4, trace_length=50)
|
|
|
|
| 26 |
use_resize: bool = True,
|
| 27 |
max_frame_size: int = DEFAULT_MAX_FRAME_SIZE,
|
| 28 |
detect_every_n: int = DEFAULT_DETECT_EVERY_N_FRAMES,
|
| 29 |
+
line_orientation: str = "Ngang",
|
| 30 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
if video_path is None:
|
| 32 |
return None
|
| 33 |
|
|
|
|
| 34 |
if isinstance(video_path, dict):
|
| 35 |
video_path = video_path.get("path", video_path)
|
| 36 |
|
|
|
|
| 37 |
video_info = sv.VideoInfo.from_video_path(video_path)
|
| 38 |
|
| 39 |
byte_tracker = sv.ByteTrack(
|
|
|
|
| 52 |
def callback(frame: np.ndarray, index: int) -> np.ndarray:
|
| 53 |
nonlocal previous_positions, class_counts, crossed_ids
|
| 54 |
|
|
|
|
| 55 |
if max_frame_size is None or max_frame_size <= 0:
|
| 56 |
max_size = DEFAULT_MAX_FRAME_SIZE
|
| 57 |
else:
|
|
|
|
| 62 |
else:
|
| 63 |
detect_every = int(detect_every_n)
|
| 64 |
|
|
|
|
| 65 |
fh_orig, fw_orig = frame.shape[:2]
|
| 66 |
if use_resize:
|
| 67 |
scale = min(1.0, max_size / max(fh_orig, fw_orig))
|
|
|
|
| 74 |
else:
|
| 75 |
frame_infer = frame
|
| 76 |
|
|
|
|
| 77 |
fh, fw = frame_infer.shape[:2]
|
|
|
|
| 78 |
if line_orientation == "Dọc":
|
| 79 |
line_pos = int(fw * 0.5)
|
| 80 |
is_horizontal = False
|
|
|
|
| 82 |
line_pos = int(fh * 0.5)
|
| 83 |
is_horizontal = True
|
| 84 |
|
|
|
|
| 85 |
if detect_every > 1 and index % detect_every != 0:
|
| 86 |
annotator_frame = frame_infer.copy()
|
| 87 |
|
|
|
|
| 88 |
if is_horizontal:
|
| 89 |
cv2.line(
|
| 90 |
annotator_frame,
|
|
|
|
| 102 |
2,
|
| 103 |
)
|
| 104 |
|
|
|
|
| 105 |
box_w, box_h = 280, 50 + len(SELECTED_CLASS_NAMES) * 28
|
| 106 |
x0, y0 = fw - box_w - 20, 20
|
| 107 |
|
|
|
|
| 138 |
detections = detections[np.isin(detections.class_id, SELECTED_CLASS_IDS)]
|
| 139 |
detections = byte_tracker.update_with_detections(detections)
|
| 140 |
|
|
|
|
| 141 |
if detections.tracker_id is not None:
|
| 142 |
xyxy = detections.xyxy
|
| 143 |
for i in range(len(detections)):
|
|
|
|
| 147 |
cx = (xyxy[i, 0] + xyxy[i, 2]) / 2
|
| 148 |
cy = (xyxy[i, 1] + xyxy[i, 3]) / 2
|
| 149 |
|
|
|
|
| 150 |
curr_coord = cy if is_horizontal else cx
|
| 151 |
|
| 152 |
if tid in previous_positions:
|
|
|
|
| 181 |
scene=annotator_frame, detections=detections, labels=labels
|
| 182 |
)
|
| 183 |
|
|
|
|
| 184 |
if is_horizontal:
|
| 185 |
cv2.line(
|
| 186 |
annotator_frame,
|
|
|
|
| 198 |
2,
|
| 199 |
)
|
| 200 |
|
|
|
|
| 201 |
box_w, box_h = 280, 50 + len(SELECTED_CLASS_NAMES) * 28
|
| 202 |
x0, y0 = fw - box_w - 20, 20
|
| 203 |
|
|
|
|
| 238 |
return output_path
|
| 239 |
|
| 240 |
|
| 241 |
+
with gr.Blocks(title="Nhận dạng phương tiện giao thông", theme=gr.themes.Soft(primary_hue="blue", secondary_hue="gray")) as demo:
|
|
|
|
|
|
|
| 242 |
with gr.Row():
|
| 243 |
gr.Markdown(
|
| 244 |
"""
|
| 245 |
<div style="display:flex;flex-direction:column;gap:4px;">
|
| 246 |
+
<h1 style="margin-bottom:4px;">🚗 Nhận diện phương tiện giao thông (YOLOv8 + ByteTrack)</h1>
|
|
|
|
|
|
|
|
|
|
| 247 |
<p style="margin:0;font-size:14px;color:#6b7280;">
|
| 248 |
Thực hiện: <strong>Trần Hải Nam - 223332840</strong>
|
| 249 |
</p>
|
|
|
|
| 263 |
with gr.Column(scale=1):
|
| 264 |
gr.Markdown(
|
| 265 |
"### ✅ Kết quả đã xử lý\n"
|
| 266 |
+
"Hiển thị và thống kê số lượng theo lớp."
|
| 267 |
)
|
| 268 |
video_output = gr.Video(label="Video đã xử lý")
|
| 269 |
|
|
|
|
| 271 |
with gr.Accordion("⚙️ Tùy chọn nâng cao", open=False):
|
| 272 |
with gr.Row():
|
| 273 |
use_resize = gr.Checkbox(
|
| 274 |
+
value=True, label="Giảm kích thước khung hình trước khi nhận dạng"
|
| 275 |
)
|
| 276 |
line_orientation = gr.Radio(
|
| 277 |
choices=["Ngang", "Dọc"],
|
| 278 |
value="Ngang",
|
| 279 |
+
label="Hướng phương tiện di chuyển",
|
| 280 |
)
|
| 281 |
with gr.Row():
|
| 282 |
max_frame_size = gr.Slider(
|
|
|
|
| 305 |
"""
|
| 306 |
---
|
| 307 |
### ℹ️ Gợi ý sử dụng
|
| 308 |
+
- Mặc định hướng phương tiện di chuyển để nhận dạng là nằm **ngang** ở giữa khung hình (50% chiều cao).
|
| 309 |
+
- Có thể chuyển sang hướng **dọc** trong phần _"Tùy chọn nâng cao"_.
|
| 310 |
+
- Vì sử dụng CPU, nên:
|
| 311 |
- Dùng video **ngắn** (< 30 giây).
|
| 312 |
- Tăng `Detect mỗi N frame` nếu muốn xử lý nhanh hơn.
|
| 313 |
+
- Model sử dụng: **YOLOv8n**.
|
| 314 |
"""
|
| 315 |
)
|
| 316 |
|