shun-ren commited on
Commit
56b41cf
·
1 Parent(s): 8dcf265

initial clean deploy

Browse files
Files changed (3) hide show
  1. .gitignore +5 -0
  2. app.py +309 -0
  3. requirements.txt +8 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pt
4
+ data/images/
5
+ raw_images/
app.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
17
+ from torchvision import transforms
18
+ import gradio as gr
19
+ from model import build_model
20
+ import cv2
21
+ 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
+
95
+ # ---- history click ----
96
+ def on_history_click(idx, history_state):
97
+ if idx < len(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://192.168.1.4: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)
122
+ threading.Thread(target=run, daemon=True).start()
123
+
124
+ def stop_live_feed():
125
+ global stop_flag
126
+ stop_flag = True
127
+
128
+ cap = None
129
+ prev_gray = None
130
+ motion_active = False
131
+ 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()
171
+ cap = None
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")
228
+ # predict_btn = gr.Button("Predict")
229
+ # # Side-by-side gallery + bar chart
230
+ # gallery_out = gr.Gallery(label="Original & Grad-CAM", columns=2, height=300)
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(
243
+ type="pil",
244
+ label="Upload Image",
245
+ height=350
246
+ )
247
+
248
+ # Load initial history
249
+ history_state = gr.State([])
250
+ with gr.Row():
251
+ history_slots = [
252
+ gr.Image(type="pil", interactive=False, height=120, width=120, label=f"#{i+1}")
253
+ for i in range(MAX_HISTORY)
254
+ ]
255
+
256
+ # Add select and click events to each history slot
257
+ for i, slot in enumerate(history_slots):
258
+ slot.select(
259
+ fn=lambda h, i=i: on_history_click(i, h),
260
+ inputs=history_state,
261
+ outputs=image_input
262
+ )
263
+
264
+ # --- Output Column ---
265
+ with gr.Column(scale=1):
266
+ gr.Markdown("<h2>Results</h2>")
267
+ predicted_label = gr.Textbox(label="Predicted Material", interactive=False)
268
+ confidence_score = gr.Textbox(label="Confidence", interactive=False)
269
+ all_scores_label = gr.Label(num_top_classes=3, label="All Confidence Scores")
270
+
271
+ # Add heatmap
272
+ heatmap_gallery = gr.Gallery(
273
+ label="Visualizations",
274
+ columns=3,
275
+ height=300
276
+ )
277
+ submit_btn = gr.Button("Classify", variant="primary")
278
+
279
+ # --- Button Logic ---
280
+ submit_btn.click(
281
+ fn=classify_and_update,
282
+ inputs=[image_input, history_state],
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=[],
302
+ outputs=[json_out_live, label_out_live, live_feed, motion_out]
303
+ )
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)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ torchvision
3
+ scikit-learn
4
+ pillow
5
+ matplotlib
6
+ gradio #latest ver
7
+ opencv-python
8
+ numpy