Commit
·
1d88641
1
Parent(s):
f214779
System update 13/5. 1. Spawn in valid zone. 2. Keep body offset (20x20). 3. Prevent skip-jump over obstacle. 4. Enhance garbage overlay. 5. Truncate KNN to focus on A*.
Browse files
app.py
CHANGED
@@ -134,7 +134,7 @@ def build_masks(seg):
|
|
134 |
return water_mask, garbage_mask, movable_mask
|
135 |
|
136 |
# Garbage mask can be highlighted in red
|
137 |
-
def highlight_chunk_masks_on_frame(frame, labels, objs, color_uncollected=(0, 0, 128), color_collected=(0, 128, 0), alpha=0.
|
138 |
"""
|
139 |
Overlays semi-transparent colored regions for garbage chunks on the frame.
|
140 |
`objs` must have 'pos' and 'col' keys. The collection status changes the overlay color.
|
@@ -148,6 +148,7 @@ def highlight_chunk_masks_on_frame(frame, labels, objs, color_uncollected=(0, 0,
|
|
148 |
mask = (labels == lab).astype(np.uint8)
|
149 |
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
150 |
color = color_collected if obj["col"] else color_uncollected
|
|
|
151 |
cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
|
152 |
# Blend overlay with original frame using alpha
|
153 |
return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
|
@@ -159,6 +160,7 @@ def highlight_water_mask_on_frame(frame, binary_mask, color=(255, 0, 0), alpha=0
|
|
159 |
overlay = frame.copy()
|
160 |
mask = binary_mask.astype(np.uint8) * 255
|
161 |
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
162 |
cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
|
163 |
return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
|
164 |
|
@@ -175,8 +177,12 @@ def astar(start, goal, occ):
|
|
175 |
return p[::-1]
|
176 |
for dx,dy in N8:
|
177 |
nx,ny=cur[0]+dx,cur[1]+dy
|
178 |
-
|
179 |
-
if occ[ny,nx]==0: continue
|
|
|
|
|
|
|
|
|
180 |
ng=g[cur]+1
|
181 |
if (nx,ny) not in g or ng<g[(nx,ny)]:
|
182 |
g[(nx,ny)]=ng
|
@@ -184,19 +190,37 @@ def astar(start, goal, occ):
|
|
184 |
heapq.heappush(openq,(f,(nx,ny)))
|
185 |
came[(nx,ny)]=cur
|
186 |
return []
|
187 |
-
|
188 |
-
# KNN fit
|
189 |
def knn_path(start, targets, occ):
|
190 |
todo = targets[:]; path=[]
|
191 |
cur = tuple(start)
|
192 |
while todo:
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
|
198 |
-
|
199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
return path
|
201 |
|
202 |
# ── Robot sprite/class -──────────────────────────────────────────────────
|
@@ -207,16 +231,23 @@ class Robot:
|
|
207 |
if self.png.shape[-1] != 4:
|
208 |
raise ValueError("Sprite image must have 4 channels (RGBA)")
|
209 |
self.png = np.array(Image.open(sprite).convert("RGBA").resize((40,40)))
|
210 |
-
self.
|
|
|
211 |
def step(self, path):
|
212 |
while path:
|
213 |
dx, dy = path[0][0] - self.pos[0], path[0][1] - self.pos[1]
|
214 |
dist = (dx * dx + dy * dy) ** 0.5
|
215 |
if dist <= self.speed:
|
216 |
self.pos = list(path.pop(0))
|
217 |
-
else:
|
218 |
r = self.speed / dist
|
219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
220 |
# Break after one logical move to avoid overshooting
|
221 |
break
|
222 |
|
@@ -461,8 +492,23 @@ def _pipeline(uid,img_path):
|
|
461 |
else: # Garbage within valid travelable zone
|
462 |
print(f"🧠 {len(centres)} garbage objects on water selected from {len(detections)} detections")
|
463 |
|
464 |
-
# 3-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
465 |
robot = Robot(SPRITE)
|
|
|
|
|
466 |
path = knn_path(robot.pos, centres, movable_mask)
|
467 |
|
468 |
# 4- Video synthesis
|
|
|
134 |
return water_mask, garbage_mask, movable_mask
|
135 |
|
136 |
# Garbage mask can be highlighted in red
|
137 |
+
def highlight_chunk_masks_on_frame(frame, labels, objs, color_uncollected=(0, 0, 128), color_collected=(0, 128, 0), alpha=0.8):
|
138 |
"""
|
139 |
Overlays semi-transparent colored regions for garbage chunks on the frame.
|
140 |
`objs` must have 'pos' and 'col' keys. The collection status changes the overlay color.
|
|
|
148 |
mask = (labels == lab).astype(np.uint8)
|
149 |
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
150 |
color = color_collected if obj["col"] else color_uncollected
|
151 |
+
# drawContours on overlay
|
152 |
cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
|
153 |
# Blend overlay with original frame using alpha
|
154 |
return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
|
|
|
160 |
overlay = frame.copy()
|
161 |
mask = binary_mask.astype(np.uint8) * 255
|
162 |
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
163 |
+
# drawContours on overlay
|
164 |
cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
|
165 |
return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
|
166 |
|
|
|
177 |
return p[::-1]
|
178 |
for dx,dy in N8:
|
179 |
nx,ny=cur[0]+dx,cur[1]+dy
|
180 |
+
# out-of-bounds / blocked
|
181 |
+
if not (0<=nx<640 and 0<=ny<640) or occ[ny,nx]==0: continue
|
182 |
+
# if diagonal, ensure both orthogonals are free
|
183 |
+
if abs(dx)==1 and abs(dy)==1:
|
184 |
+
if occ[cur[1]+dy, cur[0]]==0 or occ[cur[1], cur[0]+dx]==0:
|
185 |
+
continue
|
186 |
ng=g[cur]+1
|
187 |
if (nx,ny) not in g or ng<g[(nx,ny)]:
|
188 |
g[(nx,ny)]=ng
|
|
|
190 |
heapq.heappush(openq,(f,(nx,ny)))
|
191 |
came[(nx,ny)]=cur
|
192 |
return []
|
193 |
+
# KNN fit optimal path
|
|
|
194 |
def knn_path(start, targets, occ):
|
195 |
todo = targets[:]; path=[]
|
196 |
cur = tuple(start)
|
197 |
while todo:
|
198 |
+
# KNN follow a Greedy approach, which may not guarantee shortest path, hence only use A*
|
199 |
+
# nbrs = NearestNeighbors(n_neighbors=1).fit(todo)
|
200 |
+
# _,idx = nbrs.kneighbors([cur]); nxt=tuple(todo[idx[0][0]])
|
201 |
+
# seg = astar(cur, nxt, occ)
|
202 |
+
# if seg:
|
203 |
+
# if path and seg[0]==path[-1]: seg=seg[1:]
|
204 |
+
# path.extend(seg)
|
205 |
+
# cur = nxt; todo.remove(list(nxt))
|
206 |
+
best = None
|
207 |
+
best_len = float('inf')
|
208 |
+
best_seg = []
|
209 |
+
# Try A* to each target, find shortest actual path
|
210 |
+
for t in todo:
|
211 |
+
seg = astar(cur, tuple(t), occ)
|
212 |
+
if seg and len(seg) < best_len:
|
213 |
+
best = tuple(t)
|
214 |
+
best_len = len(seg)
|
215 |
+
best_seg = seg
|
216 |
+
if not best:
|
217 |
+
print("⚠️ Some garbage unreachable")
|
218 |
+
break # stop if no reachable targets left
|
219 |
+
if path and path[-1] == best_seg[0]:
|
220 |
+
best_seg = best_seg[1:] # avoid duplicate point
|
221 |
+
path.extend(best_seg)
|
222 |
+
cur = best
|
223 |
+
todo.remove(list(best))
|
224 |
return path
|
225 |
|
226 |
# ── Robot sprite/class -──────────────────────────────────────────────────
|
|
|
231 |
if self.png.shape[-1] != 4:
|
232 |
raise ValueError("Sprite image must have 4 channels (RGBA)")
|
233 |
self.png = np.array(Image.open(sprite).convert("RGBA").resize((40,40)))
|
234 |
+
self.speed = speed
|
235 |
+
self.pos = [20, 20] # Fallback spawn with body offset at top-left
|
236 |
def step(self, path):
|
237 |
while path:
|
238 |
dx, dy = path[0][0] - self.pos[0], path[0][1] - self.pos[1]
|
239 |
dist = (dx * dx + dy * dy) ** 0.5
|
240 |
if dist <= self.speed:
|
241 |
self.pos = list(path.pop(0))
|
242 |
+
else: # If valid path within
|
243 |
r = self.speed / dist
|
244 |
+
new_x = self.pos[0] + dx * r
|
245 |
+
new_y = self.pos[1] + dy * r
|
246 |
+
# Clip to valid region with 20px margin (for body offset)
|
247 |
+
self.pos = [
|
248 |
+
int(np.clip(new_x, 20, 640 - 20)),
|
249 |
+
int(np.clip(new_y, 20, 640 - 20))
|
250 |
+
]
|
251 |
# Break after one logical move to avoid overshooting
|
252 |
break
|
253 |
|
|
|
492 |
else: # Garbage within valid travelable zone
|
493 |
print(f"🧠 {len(centres)} garbage objects on water selected from {len(detections)} detections")
|
494 |
|
495 |
+
# 3- Robot initialization, position and navigation
|
496 |
+
# find all (y,x) within movable_mask
|
497 |
+
ys, xs = np.where(movable_mask)
|
498 |
+
if len(ys)==0:
|
499 |
+
# no travelable zone → bail out
|
500 |
+
print(f"❌ [{uid}] no water to spawn on")
|
501 |
+
video_ready[uid] = True
|
502 |
+
return
|
503 |
+
# sort by y, then x
|
504 |
+
idx = np.lexsort((xs, ys))
|
505 |
+
spawn_y, spawn_x = int(ys[idx[0]]), int(xs[idx[0]])
|
506 |
+
# enforce 20px margin so sprite never pokes out
|
507 |
+
spawn_x = np.clip(spawn_x, 20, 640-20)
|
508 |
+
spawn_y = np.clip(spawn_y, 20, 640-20)
|
509 |
robot = Robot(SPRITE)
|
510 |
+
# Robot will be spawn on the closest movable mask to top-left
|
511 |
+
robot.pos = [spawn_x, spawn_y]
|
512 |
path = knn_path(robot.pos, centres, movable_mask)
|
513 |
|
514 |
# 4- Video synthesis
|