video version
Browse files- .DS_Store +0 -0
- app.py +207 -6
- best0625.pt → best0628.pt +2 -2
- requirements.txt +3 -0
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
|
app.py
CHANGED
@@ -1,12 +1,213 @@
|
|
|
|
1 |
import torch
|
2 |
from ultralytics import YOLO
|
3 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
-
|
|
|
|
|
|
|
|
|
|
|
6 |
|
7 |
-
|
8 |
-
|
9 |
-
|
|
|
10 |
|
11 |
-
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import cv2
|
2 |
import torch
|
3 |
from ultralytics import YOLO
|
4 |
import gradio as gr
|
5 |
+
import threading
|
6 |
+
import time
|
7 |
+
import os
|
8 |
+
import zipfile
|
9 |
+
from datetime import datetime
|
10 |
+
import pandas as pd
|
11 |
+
import tempfile
|
12 |
|
13 |
+
# === 模型與設定 ===
|
14 |
+
model = YOLO("best0628.pt")
|
15 |
+
TARGET_CLASS_NAME = "kumay"
|
16 |
+
save_dir = "saved_bears"
|
17 |
+
log_path = os.path.join(save_dir, "detection_log.csv")
|
18 |
+
os.makedirs(save_dir, exist_ok=True)
|
19 |
|
20 |
+
# === 全域狀態 ===
|
21 |
+
latest_frame = None
|
22 |
+
lock = threading.Lock()
|
23 |
+
streaming = False
|
24 |
|
25 |
+
# 初始化 log 檔
|
26 |
+
if not os.path.exists(log_path):
|
27 |
+
with open(log_path, "w") as f:
|
28 |
+
f.write("frame_id,timestamp,timestamp_diff,filename,class,confidence\n")
|
29 |
+
|
30 |
+
last_detection_time = None
|
31 |
+
frame_counter = 0
|
32 |
+
|
33 |
+
# === Webcam 持續讀取 ===
|
34 |
+
def webcam_reader():
|
35 |
+
global latest_frame
|
36 |
+
cap = cv2.VideoCapture(0)
|
37 |
+
while True:
|
38 |
+
ret, frame = cap.read()
|
39 |
+
if ret:
|
40 |
+
with lock:
|
41 |
+
latest_frame = frame.copy()
|
42 |
+
time.sleep(0.03)
|
43 |
+
|
44 |
+
# === 偵測與儲存 ===
|
45 |
+
def detect_and_save(frame):
|
46 |
+
global last_detection_time, frame_counter
|
47 |
+
results = model(frame)
|
48 |
+
names = results[0].names
|
49 |
+
has_bear = False
|
50 |
+
best_conf = 0
|
51 |
+
best_cls_name = ""
|
52 |
+
|
53 |
+
for box in results[0].boxes:
|
54 |
+
cls_id = int(box.cls[0])
|
55 |
+
cls_name = names[cls_id]
|
56 |
+
conf = float(box.conf[0])
|
57 |
+
if cls_name == TARGET_CLASS_NAME and conf >= 0.85:
|
58 |
+
has_bear = True
|
59 |
+
if conf > best_conf:
|
60 |
+
best_conf = conf
|
61 |
+
best_cls_name = cls_name
|
62 |
+
|
63 |
+
if has_bear:
|
64 |
+
timestamp = datetime.now()
|
65 |
+
timestamp_str = timestamp.strftime("%Y%m%d_%H%M%S_%f")[:-3]
|
66 |
+
filename = os.path.join(save_dir, f"bear_{timestamp_str}.png")
|
67 |
+
|
68 |
+
for box in results[0].boxes:
|
69 |
+
cls_id = int(box.cls[0])
|
70 |
+
cls_name = names[cls_id]
|
71 |
+
conf = float(box.conf[0])
|
72 |
+
if cls_name == TARGET_CLASS_NAME and conf >= 0.85:
|
73 |
+
xyxy = box.xyxy[0].cpu().numpy().astype(int)
|
74 |
+
cv2.putText(
|
75 |
+
frame,
|
76 |
+
f"{cls_name}: {conf:.2f}",
|
77 |
+
(xyxy[0], xyxy[1] - 10),
|
78 |
+
cv2.FONT_HERSHEY_SIMPLEX,
|
79 |
+
0.6,
|
80 |
+
(0, 255, 0),
|
81 |
+
2,
|
82 |
+
)
|
83 |
+
cv2.rectangle(frame, (xyxy[0], xyxy[1]), (xyxy[2], xyxy[3]), (0, 255, 0), 2)
|
84 |
+
|
85 |
+
cv2.imwrite(filename, frame)
|
86 |
+
print(f"📸 偵測到 {best_cls_name},儲存:{filename}")
|
87 |
+
assert os.path.exists(filename)
|
88 |
+
|
89 |
+
diff = (timestamp - last_detection_time).total_seconds() if last_detection_time else 0.0
|
90 |
+
with open(log_path, "a") as f:
|
91 |
+
f.write(f"{frame_counter},{timestamp},{diff:.3f},{filename},{best_cls_name},{best_conf:.4f}\n")
|
92 |
+
last_detection_time = timestamp
|
93 |
+
|
94 |
+
frame_counter += 1
|
95 |
+
return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
96 |
+
|
97 |
+
# === Webcam 處理 ===
|
98 |
+
def get_annotated_frame():
|
99 |
+
global latest_frame
|
100 |
+
with lock:
|
101 |
+
frame = latest_frame.copy() if latest_frame is not None else None
|
102 |
+
if frame is None:
|
103 |
+
return None
|
104 |
+
return detect_and_save(frame)
|
105 |
+
|
106 |
+
def streaming_loop():
|
107 |
+
global streaming
|
108 |
+
while streaming:
|
109 |
+
frame = get_annotated_frame()
|
110 |
+
if frame is not None:
|
111 |
+
with lock:
|
112 |
+
cv2.imwrite("latest_stream.png", cv2.cvtColor(frame, cv2.COLOR_RGB2BGR))
|
113 |
+
time.sleep(0.2)
|
114 |
+
|
115 |
+
def start_stream():
|
116 |
+
global streaming
|
117 |
+
streaming = True
|
118 |
+
threading.Thread(target=streaming_loop, daemon=True).start()
|
119 |
+
|
120 |
+
def stop_stream():
|
121 |
+
global streaming
|
122 |
+
streaming = False
|
123 |
+
|
124 |
+
# === 影片偵測 ===
|
125 |
+
def detect_video(video_path):
|
126 |
+
cap = cv2.VideoCapture(video_path)
|
127 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
128 |
+
W = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
129 |
+
H = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
130 |
+
|
131 |
+
output_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
|
132 |
+
out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (W, H))
|
133 |
+
|
134 |
+
while cap.isOpened():
|
135 |
+
ret, frame = cap.read()
|
136 |
+
if not ret:
|
137 |
+
break
|
138 |
+
annotated = detect_and_save(frame)
|
139 |
+
out.write(cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR))
|
140 |
+
|
141 |
+
cap.release()
|
142 |
+
out.release()
|
143 |
+
print(f"✅ 影片處理完成:{output_path}")
|
144 |
+
return output_path
|
145 |
+
|
146 |
+
# === ZIP 功能 ===
|
147 |
+
def create_zip():
|
148 |
+
zip_path = "detection_package.zip"
|
149 |
+
with zipfile.ZipFile(zip_path, "w") as zipf:
|
150 |
+
for fname in os.listdir(save_dir):
|
151 |
+
fpath = os.path.join(save_dir, fname)
|
152 |
+
if os.path.isfile(fpath):
|
153 |
+
zipf.write(fpath, arcname=os.path.join("saved_bears", fname))
|
154 |
+
if os.path.exists(log_path):
|
155 |
+
zipf.write(log_path, arcname="detection_log.csv")
|
156 |
+
return zip_path
|
157 |
+
|
158 |
+
def read_csv():
|
159 |
+
if os.path.exists(log_path):
|
160 |
+
df = pd.read_csv(log_path)
|
161 |
+
if "frame_id" in df.columns:
|
162 |
+
return df.sort_values(by="frame_id", ascending=False).reset_index(drop=True)
|
163 |
+
return df
|
164 |
+
return []
|
165 |
+
|
166 |
+
def get_latest_image():
|
167 |
+
return "latest_stream.png" if os.path.exists("latest_stream.png") else None
|
168 |
+
|
169 |
+
# === 啟動 webcam 執行緒 ===
|
170 |
+
threading.Thread(target=webcam_reader, daemon=True).start()
|
171 |
+
|
172 |
+
# === Gradio UI ===
|
173 |
+
with gr.Blocks() as demo:
|
174 |
+
gr.Markdown("## 🐻 台灣黑熊偵測系統")
|
175 |
+
|
176 |
+
with gr.Tab("📹 上傳影片辨識"):
|
177 |
+
gr.Markdown("上傳影片,逐幀偵測台灣黑熊,並自動儲存出現畫面")
|
178 |
+
video_input = gr.Video()
|
179 |
+
video_output = gr.Video()
|
180 |
+
video_button = gr.Button("上傳並分析影片")
|
181 |
+
video_button.click(fn=detect_video, inputs=video_input, outputs=video_output)
|
182 |
+
|
183 |
+
with gr.Tab("📷 即時攝影機偵測"):
|
184 |
+
gr.Markdown("啟用 webcam 進行即時偵測,若出現台灣黑熊則自動儲存影像")
|
185 |
+
webcam_output = gr.Image(
|
186 |
+
label="即時辨識結果",
|
187 |
+
interactive=False,
|
188 |
+
type="filepath",
|
189 |
+
value=get_latest_image,
|
190 |
+
every=0.2
|
191 |
+
)
|
192 |
+
with gr.Row():
|
193 |
+
start_btn = gr.Button("▶️ 開始直播")
|
194 |
+
stop_btn = gr.Button("⏹ 停止直播")
|
195 |
+
start_btn.click(fn=start_stream, inputs=[], outputs=[])
|
196 |
+
stop_btn.click(fn=stop_stream, inputs=[], outputs=[])
|
197 |
+
|
198 |
+
with gr.Tab("📁 下載與預覽"):
|
199 |
+
gr.Markdown("### 預覽與下載偵測圖片與紀錄檔")
|
200 |
+
log_df = gr.Dataframe(label="detection_log.csv 預覽", interactive=False)
|
201 |
+
load_log_btn = gr.Button("🔄 重新載入紀錄檔")
|
202 |
+
load_log_btn.click(fn=read_csv, outputs=log_df)
|
203 |
+
|
204 |
+
csv_file = gr.File(value=log_path, label="⬇️ 下載 CSV 檔")
|
205 |
+
|
206 |
+
gr.Markdown("### 打包圖片與紀錄檔(.zip)")
|
207 |
+
zip_btn = gr.Button("📦 產生 ZIP 檔")
|
208 |
+
zip_file = gr.File(label="⬇️ 點我下載壓縮檔")
|
209 |
+
zip_btn.click(fn=create_zip, outputs=zip_file)
|
210 |
+
|
211 |
+
# 啟動
|
212 |
+
if __name__ == "__main__":
|
213 |
+
demo.launch()
|
best0625.pt → best0628.pt
RENAMED
@@ -1,3 +1,3 @@
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
-
oid sha256:
|
3 |
-
size
|
|
|
1 |
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:1536806a99f859860f48938f253c99edd6da12af5de188fb5ecd50ea58f89a90
|
3 |
+
size 22516707
|
requirements.txt
CHANGED
@@ -1,3 +1,6 @@
|
|
1 |
ultralytics
|
2 |
gradio
|
3 |
torch
|
|
|
|
|
|
|
|
1 |
ultralytics
|
2 |
gradio
|
3 |
torch
|
4 |
+
opencv-python
|
5 |
+
numpy
|
6 |
+
|