Skip to content

Commit 6db4e14

Browse files
committed
update pointing service
1 parent 9c0393e commit 6db4e14

1 file changed

Lines changed: 138 additions & 16 deletions

File tree

predicators/spot_utils/perception/pointing_gemini_sam2_client.py

Lines changed: 138 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import asyncio
44
import base64
55
import io
6+
import logging
67
import os
78
import time
8-
from typing import Dict, List, Union, Sequence
9+
from pathlib import Path
10+
from typing import Dict, List, Sequence, Union
911

1012
import httpx
1113
from PIL import Image, ImageDraw
@@ -85,6 +87,65 @@ def draw_points(
8587
return image
8688

8789

90+
def draw_detections(
91+
image: Image.Image,
92+
detections: List[Dict[str, Union[List[float], str]]],
93+
box_width: int = 2,
94+
text_offset: int = 10,
95+
) -> Image.Image:
96+
draw = ImageDraw.Draw(image)
97+
width, height = image.size
98+
for det in detections:
99+
box = det.get("box_2d")
100+
if not isinstance(box, list) or len(box) != 4:
101+
continue
102+
x1, y1, x2, y2 = box
103+
x1 = int(float(x1) * width / 1000)
104+
x2 = int(float(x2) * width / 1000)
105+
y1 = int(float(y1) * height / 1000)
106+
y2 = int(float(y2) * height / 1000)
107+
draw.rectangle((x1, y1, x2, y2), outline="cyan", width=box_width)
108+
label = str(det.get("label", ""))
109+
if label:
110+
draw.text((x1 + text_offset, y1 - text_offset), label, fill="cyan")
111+
return image
112+
113+
114+
_MASK_COLORS = [
115+
(255, 0, 0),
116+
(0, 200, 255),
117+
(0, 255, 127),
118+
(255, 165, 0),
119+
(186, 85, 211),
120+
]
121+
122+
123+
def overlay_masks(
124+
image: Image.Image,
125+
masks: List[Dict[str, Union[str, int]]],
126+
alpha: int = 110,
127+
) -> Image.Image:
128+
"""Blend segmentation masks onto the provided image."""
129+
if not masks:
130+
return image
131+
base = image.convert("RGBA")
132+
for idx, mask_entry in enumerate(masks):
133+
mask_b64 = mask_entry.get("mask_png")
134+
if not mask_b64:
135+
continue
136+
try:
137+
mask = Image.open(io.BytesIO(base64.b64decode(mask_b64))).convert("L")
138+
except Exception as exc: # pragma: no cover - debug helper
139+
console.print(f"[red]Failed to decode mask for visualization: {exc}[/red]")
140+
continue
141+
if mask.size != base.size:
142+
mask = mask.resize(base.size, Image.NEAREST)
143+
color = _MASK_COLORS[idx % len(_MASK_COLORS)]
144+
overlay = Image.new("RGBA", base.size, color + (alpha,))
145+
base = Image.composite(overlay, base, mask)
146+
return base.convert("RGB")
147+
148+
88149
class PointingGeminiSAM2Client:
89150
def __init__(self, host: str = "localhost", port: int = 7100):
90151
"""Initialize client with host and port."""
@@ -155,17 +216,19 @@ async def predict_async(
155216
)
156217

157218
result = response.json()
158-
entries = []
159-
count = "unknown"
160-
example = None
219+
entries: List[Dict] = []
161220
if isinstance(result, dict):
162221
entries = result.get("results") or []
163-
count = len(entries)
164-
example = entries[0].get("prompt") if entries else None
165-
if not entries:
166-
logging.warning(
167-
"[PointingClient] No detections returned (HTTP 200)"
168-
"; check server logs for details.")
222+
elif isinstance(result, list):
223+
entries = result
224+
count = len(entries)
225+
example = None
226+
if entries and isinstance(entries[0], dict):
227+
example = entries[0].get("prompt")
228+
if not entries:
229+
logging.warning(
230+
"[PointingClient] No detections returned (HTTP 200); "
231+
"check server logs for details.")
169232
extra = f", sample='{example}'" if example else ""
170233
console.print(
171234
f"[green]Server request completed successfully[/green]"
@@ -181,13 +244,19 @@ def predict(
181244
points: bool = True,
182245
segmentation: bool = False,
183246
detection: bool = False,
247+
save_visualizations: bool = False,
248+
viz_dir: str = "spot_pointing_outputs_cli",
184249
) -> Dict:
185250
"""Synchronous wrapper for predict_async."""
186251
with Timer(enable_print=False) as t:
187-
result = asyncio.run(self.predict_async(
188-
images, prompts, points, segmentation, detection
189-
))
252+
result = asyncio.run(
253+
self.predict_async(images, prompts, points, segmentation, detection))
190254
result["timings"]["total"] = t.elapsed_time
255+
if save_visualizations:
256+
try:
257+
_save_client_visualizations(result.get("results", []), viz_dir)
258+
except Exception as exc: # pragma: no cover - debug helper
259+
console.print(f"[red]Failed to save pointing visualizations: {exc}[/red]")
191260
return result
192261

193262

@@ -213,6 +282,8 @@ def predict(
213282
points: bool = typer.Option(True, help="Whether to return point coordinates"),
214283
segmentation: bool = typer.Option(False, help="Whether to perform segmentation"),
215284
detection: bool = typer.Option(False, help="Whether to perform detection instead of pointing"),
285+
save_viz: bool = typer.Option(False, "--save-viz", help="Save annotated results to disk"),
286+
viz_dir: str = typer.Option("spot_pointing_outputs_cli", "--viz-dir", help="Directory for saved visualizations"),
216287
):
217288
"""Run predictions using the PointingGeminiSAM2 service."""
218289
try:
@@ -224,7 +295,13 @@ def predict(
224295
raise typer.BadParameter("Must provide at least one image (-i) and one prompt (-p)")
225296

226297
client = PointingGeminiSAM2Client(host=host, port=port)
227-
result = client.predict(image_paths, prompts, points=points, segmentation=segmentation, detection=detection)
298+
result = client.predict(image_paths,
299+
prompts,
300+
points=points,
301+
segmentation=segmentation,
302+
detection=detection,
303+
save_visualizations=save_viz,
304+
viz_dir=viz_dir)
228305

229306
# Print results
230307
console.rule("[bold blue]Results")
@@ -236,10 +313,18 @@ def predict(
236313
console.print(f"\n[yellow]Image {img_idx + 1}, Prompt {prompt_idx + 1}: '{prompt}'[/yellow]")
237314
if res.get("points"):
238315
console.print(f"Points coordinates: {res['points']}", style="cyan")
239-
if res.get("detections"):
316+
boxes = res.get("detections")
317+
if not boxes and res.get("boxes"):
318+
boxes = [{
319+
"box_2d": box,
320+
"label": res.get("prompt", "")
321+
} for box in res["boxes"]]
322+
if boxes:
240323
console.print("\nBounding boxes:", style="cyan")
241-
for i, box in enumerate(res["detections"], 1):
324+
for i, box in enumerate(boxes, 1):
242325
console.print(f"Box {i}: {box['box_2d']} ({box['label']})", style="cyan")
326+
if res.get("masks"):
327+
console.print(f"Segmentation masks: {len(res['masks'])}", style="magenta")
243328

244329
# Print timing
245330
console.rule("[bold blue]Timing")
@@ -257,3 +342,40 @@ def predict(
257342

258343
if __name__ == "__main__":
259344
app()
345+
346+
347+
def _save_client_visualizations(entries: Union[List[Dict], Dict], directory: str) -> None:
348+
if isinstance(entries, dict):
349+
entries = [entries]
350+
if not entries:
351+
console.print("[yellow]No results to visualize.[/yellow]")
352+
return
353+
Path(directory).mkdir(parents=True, exist_ok=True)
354+
for entry in entries:
355+
image_b64 = entry.get("image")
356+
if not image_b64:
357+
continue
358+
try:
359+
image = Image.open(io.BytesIO(base64.b64decode(image_b64))).convert("RGB")
360+
except Exception as exc:
361+
console.print(f"[red]Failed to decode image for visualization: {exc}[/red]")
362+
continue
363+
if entry.get("points"):
364+
image = draw_points(image, entry["points"])
365+
detections = entry.get("detections")
366+
if not detections and entry.get("boxes"):
367+
detections = [{
368+
"box_2d": box,
369+
"label": entry.get("prompt", "")
370+
} for box in entry["boxes"]]
371+
if detections:
372+
image = draw_detections(image, detections)
373+
masks = entry.get("masks") or []
374+
if masks:
375+
image = overlay_masks(image, masks)
376+
timestamp = time.strftime("%Y%m%d-%H%M%S")
377+
prompt = entry.get("prompt", "prompt").replace(" ", "_")
378+
image_idx = entry.get("image_index", 0)
379+
filename = Path(directory) / f"client_pointing_{timestamp}_{prompt}_{image_idx}.png"
380+
image.save(filename)
381+
console.print(f"Saved pointing visualization to {filename}", style="yellow")

0 commit comments

Comments
 (0)