Spaces:
Sleeping
Sleeping
C-vll commited on
Commit ·
2d34911
1
Parent(s): 12490e5
boost cam speed
Browse files- .gradio/certificate.pem +31 -0
- app.py +95 -102
.gradio/certificate.pem
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
| 3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
| 4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
| 5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
| 6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
| 7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
| 8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
| 9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
| 10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
| 11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
| 12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
| 13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
| 14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
| 15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
| 16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
| 17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
| 18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
| 19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
| 20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
| 21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
| 22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
| 23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
| 24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
| 25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
| 26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
| 27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
| 28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
| 29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
| 30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
| 31 |
+
-----END CERTIFICATE-----
|
app.py
CHANGED
|
@@ -1,16 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
# ------------------------------
|
| 4 |
-
# Recycle Material Classifier App
|
| 5 |
-
# ------------------------------
|
| 6 |
-
# This script:
|
| 7 |
-
# 1. Loads a trained ResNet-18 model
|
| 8 |
-
# 2. Lets user upload an image or use a live IP camera
|
| 9 |
-
# 3. Classifies the item (paper/plastic/metal)
|
| 10 |
-
# 4. Shows Grad-CAM heatmaps for explainability
|
| 11 |
-
# 5. Displays classification history
|
| 12 |
-
# ------------------------------
|
| 13 |
-
|
| 14 |
import json, torch
|
| 15 |
from pathlib import Path
|
| 16 |
from PIL import Image
|
|
@@ -22,73 +10,77 @@ import threading
|
|
| 22 |
import time
|
| 23 |
from explain import generate_gradcam
|
| 24 |
|
| 25 |
-
|
| 26 |
-
stop_flag = False
|
| 27 |
|
| 28 |
-
# ----
|
| 29 |
WEIGHTS = Path("models/resnet18_best.pt")
|
| 30 |
LABELS = Path("models/labels.json")
|
| 31 |
|
| 32 |
-
# ----
|
| 33 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 34 |
|
| 35 |
-
# ----
|
| 36 |
with open(LABELS) as f:
|
| 37 |
idx2name = {int(k): v for k, v in json.load(f).items()}
|
| 38 |
class_names = [idx2name[i] for i in sorted(idx2name.keys())]
|
| 39 |
|
| 40 |
-
# ----
|
| 41 |
model = build_model(num_classes=len(class_names), freeze_backbone=False, device=device)
|
| 42 |
state = torch.load(WEIGHTS, map_location=device)
|
| 43 |
model.load_state_dict(state)
|
| 44 |
model.eval()
|
| 45 |
|
| 46 |
-
# ----
|
| 47 |
-
# Resize -> Tensor -> Normalize (same as training)
|
| 48 |
tfm = transforms.Compose([
|
| 49 |
transforms.Resize((224, 224)),
|
| 50 |
transforms.ToTensor(),
|
| 51 |
transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225]),
|
| 52 |
])
|
| 53 |
|
| 54 |
-
# ----
|
| 55 |
-
def predict(img: Image.Image):
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
# Compute probability scores for all classes
|
| 61 |
with torch.no_grad():
|
| 62 |
x = tfm(img.convert("RGB")).unsqueeze(0).to(device)
|
| 63 |
-
|
|
|
|
| 64 |
scores = {cls: float(probs[i]) for i, cls in enumerate(class_names)}
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
|
|
|
| 72 |
|
| 73 |
-
|
|
|
|
| 74 |
|
|
|
|
| 75 |
if img is None:
|
| 76 |
return [], "N/A", "N/A", {}, history_state
|
| 77 |
|
| 78 |
-
#
|
| 79 |
gallery_imgs, pred_label, conf, all_scores = predict(img)
|
| 80 |
|
| 81 |
-
#
|
| 82 |
history_state.append(img)
|
| 83 |
history_state = history_state[-MAX_HISTORY:]
|
| 84 |
|
| 85 |
-
#
|
| 86 |
padded = history_state + [None]*(MAX_HISTORY - len(history_state))
|
| 87 |
|
| 88 |
return gallery_imgs, pred_label, f"{round(conf*100)}%", all_scores, *padded, history_state
|
| 89 |
|
| 90 |
-
|
| 91 |
-
# ---- HISTORY CLICK EVENT ----
|
| 92 |
def on_history_select(evt: gr.SelectData, history_state):
|
| 93 |
return history_state[evt.index]
|
| 94 |
|
|
@@ -98,24 +90,21 @@ def on_history_click(idx, history_state):
|
|
| 98 |
return history_state[idx]
|
| 99 |
return None
|
| 100 |
|
| 101 |
-
# ---- IP
|
| 102 |
-
# Replace the IP with your phone’s IP Webcam URL
|
| 103 |
# ip_url = "http://10.132.39.1:8080/video" # replace with your phone's IP
|
| 104 |
-
#
|
| 105 |
-
ip_url = "http://
|
| 106 |
|
| 107 |
-
#
|
| 108 |
# cap = None
|
| 109 |
# prev_gray = None
|
| 110 |
-
# motion_active = False
|
| 111 |
-
# recent_preds = []
|
| 112 |
|
| 113 |
def start_live_feed():
|
| 114 |
global stop_flag
|
| 115 |
stop_flag = False
|
| 116 |
def run():
|
| 117 |
while not stop_flag:
|
| 118 |
-
outputs = live_ipcam_generator() #
|
| 119 |
json_out_live.update(outputs[0])
|
| 120 |
label_out_live.update(outputs[1])
|
| 121 |
time.sleep(0.1)
|
|
@@ -132,39 +121,45 @@ recent_preds = []
|
|
| 132 |
|
| 133 |
#ip_url = "http://10.132.39.1:8080/video"
|
| 134 |
#ip_url = "http://192.168.1.6:8080/video"
|
| 135 |
-
|
| 136 |
def live_ipcam_generator():
|
| 137 |
-
|
| 138 |
"""
|
| 139 |
Generator that yields only frames with motion detected.
|
| 140 |
-
|
|
|
|
| 141 |
"""
|
| 142 |
-
|
| 143 |
global cap, prev_gray
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
last_trigger_time = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
|
|
|
| 149 |
while True:
|
| 150 |
-
# Initialize camera if not already
|
| 151 |
-
if cap is None or not cap.isOpened():
|
| 152 |
-
try:
|
| 153 |
-
cap = cv2.VideoCapture(ip_url)
|
| 154 |
-
time.sleep(1)
|
| 155 |
-
ret, prev = cap.read()
|
| 156 |
-
if not ret or prev is None:
|
| 157 |
-
prev_gray = None
|
| 158 |
-
raise ValueError("No frame received")
|
| 159 |
-
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
|
| 160 |
-
except Exception:
|
| 161 |
-
# If camera fails, send a blank image + "offline" message
|
| 162 |
-
dummy_img = Image.new("RGB", (224, 224), (0, 0, 0))
|
| 163 |
-
yield {"label": "Camera offline", "conf": 0}, {}, [dummy_img], {"motion_level": 0}
|
| 164 |
-
time.sleep(1)
|
| 165 |
-
continue
|
| 166 |
-
|
| 167 |
-
# Read frame
|
| 168 |
ret, frame = cap.read()
|
| 169 |
if not ret or frame is None:
|
| 170 |
cap.release()
|
|
@@ -172,56 +167,60 @@ def live_ipcam_generator():
|
|
| 172 |
dummy_img = Image.new("RGB", (224, 224), (0, 0, 0))
|
| 173 |
yield {"label": "Camera disconnected", "conf": 0}, {}, [dummy_img], {"motion_level": 0}
|
| 174 |
time.sleep(1)
|
| 175 |
-
|
| 176 |
|
| 177 |
-
# Convert to grayscale for motion detection
|
| 178 |
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 179 |
if prev_gray is not None:
|
| 180 |
diff = cv2.absdiff(prev_gray, gray)
|
| 181 |
motion_level = cv2.countNonZero(cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1])
|
| 182 |
else:
|
| 183 |
motion_level = 0
|
| 184 |
-
|
| 185 |
prev_gray = gray
|
| 186 |
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
if motion_level > motion_threshold:
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
| 190 |
if current_time - last_trigger_time >= cooldown_sec:
|
| 191 |
-
|
| 192 |
-
img = img.resize((840, 480))
|
| 193 |
pred_images, pred_label, conf, scores = predict(img)
|
| 194 |
-
|
| 195 |
pred_json = {"label": pred_label, "conf": round(conf * 100, 2)}
|
| 196 |
motion_info = {"motion_level": motion_level}
|
| 197 |
|
| 198 |
last_trigger_time = current_time
|
| 199 |
-
|
| 200 |
yield pred_json, scores, pred_images, motion_info
|
| 201 |
-
else:
|
| 202 |
-
# Skip frame due to cooldown
|
| 203 |
-
continue
|
| 204 |
-
else:
|
| 205 |
-
# Skip frames without motion
|
| 206 |
-
continue
|
| 207 |
|
| 208 |
-
#
|
| 209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
# ----
|
| 213 |
css = """
|
| 214 |
footer, #footer, .footer, [data-testid="branding"] {display:none !important;}
|
| 215 |
a[href*="gradio.app"] {display:none !important;}
|
| 216 |
"""
|
| 217 |
|
| 218 |
-
# ----
|
| 219 |
with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
| 220 |
gr.Markdown("<h1>♻️ Recycle Material Classifier</h1>")
|
| 221 |
gr.Markdown("Upload a photo of a recyclable item to classify it as **paper**, **plastic**, or **metal**.")
|
| 222 |
|
| 223 |
with gr.Tabs():
|
| 224 |
-
|
| 225 |
# --- Upload Image ---
|
| 226 |
# with gr.TabItem("Upload Image"):
|
| 227 |
# img_input = gr.Image(type="pil", label=" Upload an image")
|
|
@@ -231,12 +230,9 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
|
| 231 |
# label_out = gr.Label(num_top_classes=3, label="Top-3 probabilities")
|
| 232 |
|
| 233 |
# predict_btn.click(predict, inputs=img_input, outputs=[gallery_out, label_out])
|
| 234 |
-
|
| 235 |
-
# ========== TAB 1: UPLOAD IMAGE ==========
|
| 236 |
with gr.TabItem("Upload Image"):
|
| 237 |
|
| 238 |
with gr.Row(variant="panel"):
|
| 239 |
-
|
| 240 |
# --- Input Column ---
|
| 241 |
with gr.Column(scale=1):
|
| 242 |
image_input = gr.Image(
|
|
@@ -244,7 +240,6 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
|
| 244 |
label="Upload Image",
|
| 245 |
height=350
|
| 246 |
)
|
| 247 |
-
|
| 248 |
# Load initial history
|
| 249 |
history_state = gr.State([])
|
| 250 |
with gr.Row():
|
|
@@ -283,19 +278,18 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
|
| 283 |
outputs=[heatmap_gallery, predicted_label, confidence_score, all_scores_label, *history_slots, history_state]
|
| 284 |
)
|
| 285 |
|
| 286 |
-
|
| 287 |
with gr.TabItem("Live IP Webcam"):
|
| 288 |
json_out_live = gr.JSON(label="Prediction (top class + confidence %)")
|
| 289 |
label_out_live = gr.Label(num_top_classes=3, label="Top-3 probabilities")
|
| 290 |
live_feed = gr.Gallery(label="Live Feed",
|
| 291 |
-
height=500, #
|
| 292 |
columns=1 # 1 image per row
|
| 293 |
)
|
| 294 |
motion_out = gr.JSON(label="Motion Info")
|
| 295 |
-
start_btn = gr.Button("Start Live Feed
|
| 296 |
stop_btn = gr.Button("Stop Live Feed")
|
| 297 |
|
| 298 |
-
# Start live feed (motion-triggered)
|
| 299 |
start_btn.click(
|
| 300 |
live_ipcam_generator,
|
| 301 |
inputs=[],
|
|
@@ -304,6 +298,5 @@ with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
|
| 304 |
|
| 305 |
# Stop button can just close the browser tab or set a global stop flag
|
| 306 |
|
| 307 |
-
# ---- RUN THE APP ----
|
| 308 |
if __name__ == "__main__":
|
| 309 |
-
demo.launch(inbrowser=True
|
|
|
|
| 1 |
+
# src/app.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
import json, torch
|
| 3 |
from pathlib import Path
|
| 4 |
from PIL import Image
|
|
|
|
| 10 |
import time
|
| 11 |
from explain import generate_gradcam
|
| 12 |
|
| 13 |
+
stop_flag = False # global flag to stop the thread
|
|
|
|
| 14 |
|
| 15 |
+
# ---- paths ----
|
| 16 |
WEIGHTS = Path("models/resnet18_best.pt")
|
| 17 |
LABELS = Path("models/labels.json")
|
| 18 |
|
| 19 |
+
# ---- device ----
|
| 20 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 21 |
|
| 22 |
+
# ---- labels ----
|
| 23 |
with open(LABELS) as f:
|
| 24 |
idx2name = {int(k): v for k, v in json.load(f).items()}
|
| 25 |
class_names = [idx2name[i] for i in sorted(idx2name.keys())]
|
| 26 |
|
| 27 |
+
# ---- model ----
|
| 28 |
model = build_model(num_classes=len(class_names), freeze_backbone=False, device=device)
|
| 29 |
state = torch.load(WEIGHTS, map_location=device)
|
| 30 |
model.load_state_dict(state)
|
| 31 |
model.eval()
|
| 32 |
|
| 33 |
+
# ---- transforms (same as eval) ----
|
|
|
|
| 34 |
tfm = transforms.Compose([
|
| 35 |
transforms.Resize((224, 224)),
|
| 36 |
transforms.ToTensor(),
|
| 37 |
transforms.Normalize([0.485, 0.456, 0.406],[0.229, 0.224, 0.225]),
|
| 38 |
])
|
| 39 |
|
| 40 |
+
# ---- prediction and heatmaps ----
|
| 41 |
+
def predict(img: Image.Image, use_gradcam=True):
|
| 42 |
+
"""
|
| 43 |
+
Predicts the class of the image.
|
| 44 |
+
If use_gradcam=False, skips Grad-CAM for speed.
|
| 45 |
+
"""
|
|
|
|
| 46 |
with torch.no_grad():
|
| 47 |
x = tfm(img.convert("RGB")).unsqueeze(0).to(device)
|
| 48 |
+
outputs = model(x)
|
| 49 |
+
probs = torch.softmax(outputs, dim=1).squeeze(0).cpu().tolist()
|
| 50 |
scores = {cls: float(probs[i]) for i, cls in enumerate(class_names)}
|
| 51 |
+
top_idx = int(torch.argmax(outputs, dim=1).item())
|
| 52 |
+
pred_label = class_names[top_idx]
|
| 53 |
+
conf = scores[pred_label]
|
| 54 |
|
| 55 |
+
if use_gradcam:
|
| 56 |
+
overlay, heatmap, _, _ = generate_gradcam(img, model, device, class_names)
|
| 57 |
+
gallery = [img, overlay, heatmap]
|
| 58 |
+
else:
|
| 59 |
+
# No Grad-CAM, just return original image once
|
| 60 |
+
gallery = [img]
|
| 61 |
|
| 62 |
+
return gallery, pred_label, conf, scores
|
| 63 |
|
| 64 |
+
# ---- history ----
|
| 65 |
+
MAX_HISTORY = 12
|
| 66 |
|
| 67 |
+
def classify_and_update(img, history_state):
|
| 68 |
if img is None:
|
| 69 |
return [], "N/A", "N/A", {}, history_state
|
| 70 |
|
| 71 |
+
# run classification
|
| 72 |
gallery_imgs, pred_label, conf, all_scores = predict(img)
|
| 73 |
|
| 74 |
+
# update history
|
| 75 |
history_state.append(img)
|
| 76 |
history_state = history_state[-MAX_HISTORY:]
|
| 77 |
|
| 78 |
+
# pad with None for empty slots
|
| 79 |
padded = history_state + [None]*(MAX_HISTORY - len(history_state))
|
| 80 |
|
| 81 |
return gallery_imgs, pred_label, f"{round(conf*100)}%", all_scores, *padded, history_state
|
| 82 |
|
| 83 |
+
# ---- history select ----
|
|
|
|
| 84 |
def on_history_select(evt: gr.SelectData, history_state):
|
| 85 |
return history_state[evt.index]
|
| 86 |
|
|
|
|
| 90 |
return history_state[idx]
|
| 91 |
return None
|
| 92 |
|
| 93 |
+
# ---- IP Webcam setup ----
|
|
|
|
| 94 |
# ip_url = "http://10.132.39.1:8080/video" # replace with your phone's IP
|
| 95 |
+
#ip_url = "http://192.168.1.6:8080/video"
|
| 96 |
+
ip_url = "http://192.168.1.4:8080/video"
|
| 97 |
|
| 98 |
+
# ip_url = "http://10.132.39.1:8080/video"
|
| 99 |
# cap = None
|
| 100 |
# prev_gray = None
|
|
|
|
|
|
|
| 101 |
|
| 102 |
def start_live_feed():
|
| 103 |
global stop_flag
|
| 104 |
stop_flag = False
|
| 105 |
def run():
|
| 106 |
while not stop_flag:
|
| 107 |
+
outputs = live_ipcam_generator() # returns (json_dict, label_dict)
|
| 108 |
json_out_live.update(outputs[0])
|
| 109 |
label_out_live.update(outputs[1])
|
| 110 |
time.sleep(0.1)
|
|
|
|
| 121 |
|
| 122 |
#ip_url = "http://10.132.39.1:8080/video"
|
| 123 |
#ip_url = "http://192.168.1.6:8080/video"
|
|
|
|
| 124 |
def live_ipcam_generator():
|
|
|
|
| 125 |
"""
|
| 126 |
Generator that yields only frames with motion detected.
|
| 127 |
+
Optimized for smoother live video: skips idle frames and
|
| 128 |
+
avoids Grad-CAM unless motion just stopped.
|
| 129 |
"""
|
|
|
|
| 130 |
global cap, prev_gray
|
| 131 |
|
| 132 |
+
# --- Tunable performance parameters ---
|
| 133 |
+
motion_threshold = 50 # ↓ smaller → more sensitive motion detection
|
| 134 |
+
cooldown_sec = 0.2 # ↓ shorter → more frequent detections
|
| 135 |
+
frame_resize = (480, 360) # ↓ smaller → faster model + less network lag
|
| 136 |
+
sleep_time = 0.005 # ↓ smaller → higher FPS, ↑ → lower CPU load
|
| 137 |
+
gradcam_on_idle = True # True = run Grad-CAM once after motion stops
|
| 138 |
+
|
| 139 |
last_trigger_time = 0
|
| 140 |
+
last_motion_time = 0
|
| 141 |
+
motion_active = False
|
| 142 |
+
|
| 143 |
+
# Initialize camera once
|
| 144 |
+
if cap is None or not cap.isOpened():
|
| 145 |
+
try:
|
| 146 |
+
cap = cv2.VideoCapture(ip_url)
|
| 147 |
+
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # don't build up old frames
|
| 148 |
+
cap.set(cv2.CAP_PROP_FPS, 30) # request 30 FPS
|
| 149 |
+
time.sleep(1)
|
| 150 |
+
ret, prev = cap.read()
|
| 151 |
+
if not ret or prev is None:
|
| 152 |
+
prev_gray = None
|
| 153 |
+
raise ValueError("No frame received")
|
| 154 |
+
prev_gray = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
|
| 155 |
+
except Exception:
|
| 156 |
+
dummy_img = Image.new("RGB", (224, 224), (0, 0, 0))
|
| 157 |
+
yield {"label": "Camera offline", "conf": 0}, {}, [dummy_img], {"motion_level": 0}
|
| 158 |
+
time.sleep(1)
|
| 159 |
+
return
|
| 160 |
|
| 161 |
+
# --- Frame loop ---
|
| 162 |
while True:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
ret, frame = cap.read()
|
| 164 |
if not ret or frame is None:
|
| 165 |
cap.release()
|
|
|
|
| 167 |
dummy_img = Image.new("RGB", (224, 224), (0, 0, 0))
|
| 168 |
yield {"label": "Camera disconnected", "conf": 0}, {}, [dummy_img], {"motion_level": 0}
|
| 169 |
time.sleep(1)
|
| 170 |
+
return
|
| 171 |
|
|
|
|
| 172 |
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
| 173 |
if prev_gray is not None:
|
| 174 |
diff = cv2.absdiff(prev_gray, gray)
|
| 175 |
motion_level = cv2.countNonZero(cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)[1])
|
| 176 |
else:
|
| 177 |
motion_level = 0
|
|
|
|
| 178 |
prev_gray = gray
|
| 179 |
|
| 180 |
+
current_time = time.time()
|
| 181 |
+
img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)).resize((224, 224))
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# --- Motion detected ---
|
| 185 |
if motion_level > motion_threshold:
|
| 186 |
+
motion_active = True
|
| 187 |
+
last_motion_time = current_time
|
| 188 |
+
|
| 189 |
+
# Enforce cooldown so we don't overprocess
|
| 190 |
if current_time - last_trigger_time >= cooldown_sec:
|
| 191 |
+
use_gradcam = False # disable GradCAM during motion
|
|
|
|
| 192 |
pred_images, pred_label, conf, scores = predict(img)
|
|
|
|
| 193 |
pred_json = {"label": pred_label, "conf": round(conf * 100, 2)}
|
| 194 |
motion_info = {"motion_level": motion_level}
|
| 195 |
|
| 196 |
last_trigger_time = current_time
|
|
|
|
| 197 |
yield pred_json, scores, pred_images, motion_info
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
+
# --- Motion stopped, optionally run GradCAM once ---
|
| 200 |
+
elif motion_active and gradcam_on_idle:
|
| 201 |
+
if current_time - last_motion_time > 0.3: # short delay after motion ends
|
| 202 |
+
motion_active = False
|
| 203 |
+
use_gradcam = True
|
| 204 |
+
pred_images, pred_label, conf, scores = predict(img)
|
| 205 |
+
pred_json = {"label": f"{pred_label} (GradCAM)", "conf": round(conf * 100, 2)}
|
| 206 |
+
motion_info = {"motion_level": motion_level}
|
| 207 |
+
yield pred_json, scores, pred_images, motion_info
|
| 208 |
|
| 209 |
+
# tiny sleep to limit CPU usage
|
| 210 |
+
time.sleep(sleep_time)
|
| 211 |
|
| 212 |
+
# ---- minimal CSS ----
|
| 213 |
css = """
|
| 214 |
footer, #footer, .footer, [data-testid="branding"] {display:none !important;}
|
| 215 |
a[href*="gradio.app"] {display:none !important;}
|
| 216 |
"""
|
| 217 |
|
| 218 |
+
# ---- New Gradio Blocks UI ----
|
| 219 |
with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
|
| 220 |
gr.Markdown("<h1>♻️ Recycle Material Classifier</h1>")
|
| 221 |
gr.Markdown("Upload a photo of a recyclable item to classify it as **paper**, **plastic**, or **metal**.")
|
| 222 |
|
| 223 |
with gr.Tabs():
|
|
|
|
| 224 |
# --- Upload Image ---
|
| 225 |
# with gr.TabItem("Upload Image"):
|
| 226 |
# img_input = gr.Image(type="pil", label=" Upload an image")
|
|
|
|
| 230 |
# label_out = gr.Label(num_top_classes=3, label="Top-3 probabilities")
|
| 231 |
|
| 232 |
# predict_btn.click(predict, inputs=img_input, outputs=[gallery_out, label_out])
|
|
|
|
|
|
|
| 233 |
with gr.TabItem("Upload Image"):
|
| 234 |
|
| 235 |
with gr.Row(variant="panel"):
|
|
|
|
| 236 |
# --- Input Column ---
|
| 237 |
with gr.Column(scale=1):
|
| 238 |
image_input = gr.Image(
|
|
|
|
| 240 |
label="Upload Image",
|
| 241 |
height=350
|
| 242 |
)
|
|
|
|
| 243 |
# Load initial history
|
| 244 |
history_state = gr.State([])
|
| 245 |
with gr.Row():
|
|
|
|
| 278 |
outputs=[heatmap_gallery, predicted_label, confidence_score, all_scores_label, *history_slots, history_state]
|
| 279 |
)
|
| 280 |
|
| 281 |
+
# --- Live IP Webcam ---
|
| 282 |
with gr.TabItem("Live IP Webcam"):
|
| 283 |
json_out_live = gr.JSON(label="Prediction (top class + confidence %)")
|
| 284 |
label_out_live = gr.Label(num_top_classes=3, label="Top-3 probabilities")
|
| 285 |
live_feed = gr.Gallery(label="Live Feed",
|
| 286 |
+
height=500, # adjust to fit your page
|
| 287 |
columns=1 # 1 image per row
|
| 288 |
)
|
| 289 |
motion_out = gr.JSON(label="Motion Info")
|
| 290 |
+
start_btn = gr.Button("Start Live Feed")
|
| 291 |
stop_btn = gr.Button("Stop Live Feed")
|
| 292 |
|
|
|
|
| 293 |
start_btn.click(
|
| 294 |
live_ipcam_generator,
|
| 295 |
inputs=[],
|
|
|
|
| 298 |
|
| 299 |
# Stop button can just close the browser tab or set a global stop flag
|
| 300 |
|
|
|
|
| 301 |
if __name__ == "__main__":
|
| 302 |
+
demo.launch(inbrowser=True)
|