C-vll commited on
Commit
2d34911
·
1 Parent(s): 12490e5

boost cam speed

Browse files
Files changed (2) hide show
  1. .gradio/certificate.pem +31 -0
  2. app.py +95 -102
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ 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
31
+ -----END CERTIFICATE-----
app.py CHANGED
@@ -1,16 +1,4 @@
1
- # python .\src\app.py
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
- # ---- GLOBAL FLAG (used to stop live feed thread) ---
26
- stop_flag = False
27
 
28
- # ---- MODEL FILE PATHS ----
29
  WEIGHTS = Path("models/resnet18_best.pt")
30
  LABELS = Path("models/labels.json")
31
 
32
- # ---- SELECT DEVICE (GPU if available) ----
33
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
34
 
35
- # ---- LOAD LABELS ----
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
- # ---- LOAD MODEL ----
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
- # ---- IMAGE TRANSFORMATIONS ----
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
- # ---- PREDICTION FUNCTION ----
55
- def predict(img: Image.Image):
56
-
57
- # Generate Grad-CAM heatmaps (explainable visualization)
58
- overlay, heatmap, pred_label, conf = generate_gradcam(img, model, device, class_names)
59
-
60
- # Compute probability scores for all classes
61
  with torch.no_grad():
62
  x = tfm(img.convert("RGB")).unsqueeze(0).to(device)
63
- probs = torch.softmax(model(x), dim=1).squeeze(0).cpu().tolist()
 
64
  scores = {cls: float(probs[i]) for i, cls in enumerate(class_names)}
65
- top = max(scores, key=scores.get)
66
-
67
- return [img, overlay, heatmap], pred_label, conf, scores
68
 
69
- # ---- HISTORY SETTINGS ----
70
- MAX_HISTORY = 12 # show up to 12 previous uploads
 
 
 
 
71
 
 
72
 
73
- def classify_and_update(img, history_state):
 
74
 
 
75
  if img is None:
76
  return [], "N/A", "N/A", {}, history_state
77
 
78
- # Run classification
79
  gallery_imgs, pred_label, conf, all_scores = predict(img)
80
 
81
- # Update history (keep last 12 images)
82
  history_state.append(img)
83
  history_state = history_state[-MAX_HISTORY:]
84
 
85
- # Pad empty slots
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 CAMERA SETUP ----
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
- # ip_url = "http://192.168.1.6:8080/video"
105
- ip_url = "http://10.132.39.1:8080/video"
106
 
107
- # Variables for motion detection
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() # Returns (json_dict, label_dict)
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
- Skips all frames without meaningful motion.
 
141
  """
142
-
143
  global cap, prev_gray
144
 
145
- motion_threshold = 100 # How sensitive to motion
146
- cooldown_sec = 0.5 # Avoid multiple detections per second
 
 
 
 
 
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
- continue
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
- # Only process frames with motion above threshold
 
 
 
 
188
  if motion_level > motion_threshold:
189
- current_time = time.time()
 
 
 
190
  if current_time - last_trigger_time >= cooldown_sec:
191
- img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
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
- # tiny sleep to avoid hogging CPU
209
- time.sleep(0.01)
 
 
 
 
 
 
 
210
 
 
 
211
 
212
- # ---- SIMPLE CSS (hide Gradio footer) ----
213
  css = """
214
  footer, #footer, .footer, [data-testid="branding"] {display:none !important;}
215
  a[href*="gradio.app"] {display:none !important;}
216
  """
217
 
218
- # ---- GRADIO APP LAYOUT ----
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
- # ========== TAB 2: LIVE CAMERA ==========
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, # Adjust to fit your page
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, share=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)