Tremick commited on
Commit
6d42065
·
verified ·
1 Parent(s): f42394d

Upload 3 files

Browse files
Files changed (2) hide show
  1. app.py +299 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Optical Flow Server for Hugging Face Spaces
3
+ ============================================
4
+ Complete server for cloud-based optical flow processing.
5
+ Deploy this directly to Hugging Face Spaces.
6
+ """
7
+
8
+ import gradio as gr
9
+ import cv2
10
+ import numpy as np
11
+ import base64
12
+ from io import BytesIO
13
+ from PIL import Image
14
+ from sklearn.cluster import DBSCAN
15
+ import colorsys
16
+ from collections import deque
17
+ import uuid
18
+ import time
19
+
20
+
21
+ class ServerOpticalFlowEngine:
22
+ """Optical Flow Engine optimized for server-side processing."""
23
+
24
+ def __init__(self):
25
+ self.prev_gray = None
26
+ self.trail_points = {}
27
+ self.max_trail_length = 30
28
+
29
+ self.bg_subtractor = cv2.createBackgroundSubtractorMOG2(
30
+ history=500, varThreshold=16, detectShadows=True
31
+ )
32
+ self.color_pool = self._generate_colors(20)
33
+
34
+ def _generate_colors(self, n):
35
+ colors = []
36
+ for i in range(n):
37
+ hue = i / n
38
+ rgb = colorsys.hsv_to_rgb(hue, 0.9, 0.95)
39
+ bgr = (int(rgb[2] * 255), int(rgb[1] * 255), int(rgb[0] * 255))
40
+ colors.append(bgr)
41
+ return colors
42
+
43
+ def compute_flow(self, gray):
44
+ if self.prev_gray is None:
45
+ self.prev_gray = gray.copy()
46
+ return None
47
+
48
+ flow = cv2.calcOpticalFlowFarneback(
49
+ self.prev_gray, gray, None,
50
+ pyr_scale=0.5, levels=5, winsize=13,
51
+ iterations=10, poly_n=5, poly_sigma=1.1,
52
+ flags=cv2.OPTFLOW_FARNEBACK_GAUSSIAN
53
+ )
54
+ self.prev_gray = gray.copy()
55
+ return flow
56
+
57
+ def segment_motion(self, frame):
58
+ fg_mask = self.bg_subtractor.apply(frame)
59
+ fg_mask[fg_mask == 127] = 0
60
+
61
+ kernel_small = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
62
+ fg_mask = cv2.erode(fg_mask, kernel_small, iterations=2)
63
+
64
+ kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
65
+ fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, kernel)
66
+
67
+ kernel_large = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
68
+ fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel_large)
69
+
70
+ fg_mask = cv2.dilate(fg_mask, kernel_small, iterations=1)
71
+ return fg_mask
72
+
73
+ def detect_objects(self, mask, flow=None, min_area=2000):
74
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
75
+
76
+ if not contours:
77
+ return None, {}
78
+
79
+ label_image = np.full(mask.shape, -1, dtype=np.int32)
80
+ objects = {}
81
+ label_id = 0
82
+
83
+ for contour in contours:
84
+ area = cv2.contourArea(contour)
85
+ if area < min_area:
86
+ continue
87
+
88
+ x, y, w, h = cv2.boundingRect(contour)
89
+ aspect_ratio = float(h) / w if w > 0 else 0
90
+ if aspect_ratio < 0.2 or aspect_ratio > 8.0:
91
+ continue
92
+
93
+ hull = cv2.convexHull(contour)
94
+ hull_area = cv2.contourArea(hull)
95
+ if hull_area > 0 and (area / hull_area) < 0.3:
96
+ continue
97
+
98
+ cv2.drawContours(label_image, [contour], -1, label_id, -1)
99
+
100
+ M = cv2.moments(contour)
101
+ cx = int(M['m10'] / M['m00']) if M['m00'] > 0 else x + w // 2
102
+ cy = int(M['m01'] / M['m00']) if M['m00'] > 0 else y + h // 2
103
+
104
+ avg_velocity = (0, 0)
105
+ if flow is not None:
106
+ contour_mask = np.zeros(mask.shape, dtype=np.uint8)
107
+ cv2.drawContours(contour_mask, [contour], -1, 255, -1)
108
+ flow_x = flow[:, :, 0][contour_mask > 0]
109
+ flow_y = flow[:, :, 1][contour_mask > 0]
110
+ if len(flow_x) > 0:
111
+ avg_velocity = (float(np.mean(flow_x)), float(np.mean(flow_y)))
112
+
113
+ objects[label_id] = {
114
+ 'centroid': (cx, cy),
115
+ 'bbox': (x, y, x + w, y + h),
116
+ 'velocity': avg_velocity,
117
+ 'area': area,
118
+ 'contour': contour
119
+ }
120
+ label_id += 1
121
+
122
+ return label_image, objects if objects else (None, {})
123
+
124
+ def update_trails(self, objects):
125
+ current_ids = set(objects.keys())
126
+ for old_id in set(self.trail_points.keys()) - current_ids:
127
+ del self.trail_points[old_id]
128
+
129
+ for obj_id, obj in objects.items():
130
+ if obj_id not in self.trail_points:
131
+ self.trail_points[obj_id] = deque(maxlen=self.max_trail_length)
132
+ self.trail_points[obj_id].append(obj['centroid'])
133
+
134
+ def draw_results(self, frame, objects, label_image):
135
+ output = frame.copy()
136
+
137
+ for label, obj in objects.items():
138
+ color = self.color_pool[label % len(self.color_pool)]
139
+ x1, y1, x2, y2 = obj['bbox']
140
+ cv2.rectangle(output, (x1, y1), (x2, y2), color, 2)
141
+
142
+ cx, cy = obj['centroid']
143
+ cv2.circle(output, (cx, cy), 5, color, -1)
144
+
145
+ vx, vy = obj['velocity']
146
+ if np.sqrt(vx**2 + vy**2) > 1:
147
+ cv2.arrowedLine(output, (cx, cy),
148
+ (int(cx + vx * 5), int(cy + vy * 5)),
149
+ (0, 255, 255), 3, tipLength=0.3)
150
+
151
+ cv2.putText(output, f"Obj {label+1}", (x1, y1 - 25),
152
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
153
+ cv2.putText(output, f"Speed: {np.sqrt(vx**2 + vy**2):.1f}",
154
+ (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
155
+
156
+ if label_image is not None:
157
+ mask = (label_image == label).astype(np.uint8) * 255
158
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
159
+ cv2.drawContours(output, contours, -1, color, 2)
160
+
161
+ return output
162
+
163
+ def draw_trails(self, frame):
164
+ output = frame.copy()
165
+ for obj_id, trail in self.trail_points.items():
166
+ if len(trail) < 2:
167
+ continue
168
+ color = self.color_pool[obj_id % len(self.color_pool)]
169
+ points = np.array(list(trail), dtype=np.int32)
170
+ for i in range(1, len(points)):
171
+ thickness = int(1 + (i / len(points)) * 3)
172
+ cv2.line(output, tuple(points[i-1]), tuple(points[i]), color, thickness)
173
+ cv2.circle(output, tuple(points[-1]), 6, color, -1)
174
+ return output
175
+
176
+ def compute_heatmap(self, flow):
177
+ if flow is None:
178
+ return None
179
+ magnitude = np.sqrt(flow[..., 0]**2 + flow[..., 1]**2)
180
+ normalized = cv2.normalize(magnitude, None, 0, 255, cv2.NORM_MINMAX)
181
+ return cv2.applyColorMap(normalized.astype(np.uint8), cv2.COLORMAP_HOT)
182
+
183
+ def process_frame(self, frame):
184
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
185
+ flow = self.compute_flow(gray)
186
+ motion_mask = self.segment_motion(frame)
187
+
188
+ results = {
189
+ 'num_objects': 0,
190
+ 'tracked': frame.copy(),
191
+ 'trails': frame.copy(),
192
+ 'heatmap': np.zeros_like(frame)
193
+ }
194
+
195
+ if flow is not None:
196
+ results['heatmap'] = self.compute_heatmap(flow)
197
+ label_image, objects = self.detect_objects(motion_mask, flow)
198
+
199
+ if objects:
200
+ results['num_objects'] = len(objects)
201
+ self.update_trails(objects)
202
+ results['tracked'] = self.draw_results(frame, objects, label_image)
203
+ results['trails'] = self.draw_trails(results['tracked'])
204
+
205
+ return results
206
+
207
+ def reset(self):
208
+ self.prev_gray = None
209
+ self.trail_points = {}
210
+
211
+
212
+ # Session management
213
+ sessions = {}
214
+ SESSION_TIMEOUT = 300
215
+
216
+
217
+ def get_or_create_session(session_id):
218
+ current_time = time.time()
219
+ expired = [sid for sid, data in sessions.items()
220
+ if current_time - data['last_access'] > SESSION_TIMEOUT]
221
+ for sid in expired:
222
+ del sessions[sid]
223
+
224
+ if session_id not in sessions:
225
+ sessions[session_id] = {
226
+ 'engine': ServerOpticalFlowEngine(),
227
+ 'last_access': current_time
228
+ }
229
+ else:
230
+ sessions[session_id]['last_access'] = current_time
231
+
232
+ return sessions[session_id]['engine']
233
+
234
+
235
+ def decode_frame(base64_data):
236
+ img_data = base64.b64decode(base64_data)
237
+ img = Image.open(BytesIO(img_data))
238
+ return cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
239
+
240
+
241
+ def encode_frame(frame):
242
+ _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
243
+ return base64.b64encode(buffer).decode('utf-8')
244
+
245
+
246
+ def process_frame_api(frame_base64, session_id):
247
+ """Process a single frame."""
248
+ try:
249
+ engine = get_or_create_session(session_id)
250
+ frame = decode_frame(frame_base64)
251
+ results = engine.process_frame(frame)
252
+
253
+ return {
254
+ 'success': True,
255
+ 'num_objects': results['num_objects'],
256
+ 'tracked': encode_frame(results['tracked']),
257
+ 'trails': encode_frame(results['trails']),
258
+ 'heatmap': encode_frame(results['heatmap'])
259
+ }
260
+ except Exception as e:
261
+ return {'success': False, 'error': str(e)}
262
+
263
+
264
+ def reset_session_api(session_id):
265
+ """Reset session state."""
266
+ if session_id in sessions:
267
+ sessions[session_id]['engine'].reset()
268
+ return {'success': True}
269
+
270
+
271
+ def create_new_session():
272
+ """Create new session."""
273
+ session_id = str(uuid.uuid4())
274
+ get_or_create_session(session_id)
275
+ return session_id
276
+
277
+
278
+ # Gradio interface
279
+ with gr.Blocks(title="Optical Flow Server") as demo:
280
+ gr.Markdown("# 🎥 Optical Flow Processing Server")
281
+ gr.Markdown("Use with: `python optical_flow_advanced.py --server <this-url>`")
282
+
283
+ with gr.Tab("Test"):
284
+ with gr.Row():
285
+ input_image = gr.Image(label="Input Frame", type="numpy")
286
+ output_image = gr.Image(label="Processed")
287
+
288
+ def test_process(image):
289
+ if image is None:
290
+ return None
291
+ engine = ServerOpticalFlowEngine()
292
+ engine.process_frame(image)
293
+ return engine.process_frame(image)['tracked']
294
+
295
+ input_image.change(test_process, inputs=input_image, outputs=output_image)
296
+
297
+
298
+ if __name__ == "__main__":
299
+ demo.launch(server_name="0.0.0.0", server_port=7860)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Server dependencies for Hugging Face Spaces
2
+ gradio>=4.0.0
3
+ opencv-python-headless>=4.5.0
4
+ numpy>=1.19.0
5
+ scikit-learn>=0.24.0
6
+ Pillow>=8.0.0