Skip to content

Commit a40f8ce

Browse files
committed
add client side for gemini pointing + sam2
1 parent 9c2db03 commit a40f8ce

1 file changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
"""Client for Gemini pointing capability with optional SAM2 segmentation."""
2+
3+
import asyncio
4+
import base64
5+
import io
6+
import os
7+
import time
8+
from typing import Dict, List, Union, Sequence
9+
10+
import httpx
11+
from PIL import Image, ImageDraw
12+
from rich.console import Console
13+
from rich.progress import Progress, SpinnerColumn, TextColumn
14+
import typer
15+
16+
# Initialize console globally
17+
console = Console()
18+
19+
20+
class Timer:
21+
"""Simple timer context manager."""
22+
def __init__(self, enable_print: bool = True):
23+
self.enable_print = enable_print
24+
self.start_time = 0.0
25+
self.elapsed_time = 0.0
26+
27+
def __enter__(self):
28+
self.start_time = time.time()
29+
return self
30+
31+
def __exit__(self, *args):
32+
self.elapsed_time = time.time() - self.start_time
33+
if self.enable_print:
34+
print(f"Elapsed time: {self.elapsed_time:.3f}s")
35+
36+
37+
def encode_image(image: Union[str, Image.Image]) -> str:
38+
"""Encode image to base64 string."""
39+
if isinstance(image, str):
40+
with open(image, "rb") as image_file:
41+
return base64.b64encode(image_file.read()).decode()
42+
elif isinstance(image, Image.Image):
43+
buffered = io.BytesIO()
44+
image.save(buffered, format="PNG")
45+
return base64.b64encode(buffered.getvalue()).decode()
46+
else:
47+
raise ValueError("Image must be either a file path or PIL Image")
48+
49+
50+
def draw_points(
51+
image: Image.Image,
52+
points_data: List[Dict[str, Union[List[float], str]]],
53+
point_radius: int = 10,
54+
text_offset: int = 15,
55+
) -> Image.Image:
56+
"""Draw points and labels on the image with enhanced visibility."""
57+
draw = ImageDraw.Draw(image)
58+
width, height = image.size
59+
60+
for point_data in points_data:
61+
point = point_data["point"]
62+
label = str(point_data["label"])
63+
64+
# Denormalize coordinates from 0-1000 range to image coordinates
65+
y = int(float(point[0]) * height / 1000)
66+
x = int(float(point[1]) * width / 1000)
67+
68+
# Draw point with larger radius and white outline
69+
draw.ellipse(
70+
[x - point_radius, y - point_radius, x + point_radius, y + point_radius],
71+
fill="red",
72+
outline="white",
73+
width=2,
74+
)
75+
76+
# Draw label with stroke for better visibility
77+
draw.text(
78+
(x + text_offset, y - text_offset),
79+
label,
80+
fill="red",
81+
stroke_width=2,
82+
stroke_fill="white",
83+
)
84+
85+
return image
86+
87+
88+
class PointingGeminiSAM2Client:
89+
def __init__(self, host: str = "localhost", port: int = 7100):
90+
"""Initialize client with host and port."""
91+
self.endpoint = f"http://{host}:{port}/pointing_gemini_sam2_service"
92+
93+
async def predict_async(
94+
self,
95+
images: Union[str, Image.Image, Sequence[Union[str, Image.Image]]],
96+
prompts: Union[str, Sequence[str]],
97+
points: bool = True,
98+
segmentation: bool = False,
99+
detection: bool = False,
100+
) -> Dict:
101+
"""Async prediction with support for multiple images and prompts."""
102+
timings = {}
103+
console.rule("[bold blue]Starting PointingGeminiSAM2 Client Request")
104+
console.print(f"Endpoint: {self.endpoint}", style="cyan")
105+
106+
# Convert single inputs to lists
107+
if isinstance(images, (str, Image.Image)):
108+
images = [images]
109+
if isinstance(prompts, str):
110+
prompts = [prompts]
111+
112+
# Process images
113+
processed_images = []
114+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
115+
task = progress.add_task("Processing images...", total=len(images))
116+
117+
with Timer(enable_print=False) as t:
118+
for i, img in enumerate(images):
119+
if isinstance(img, str):
120+
progress.update(task, description=f"Loading image {i+1}: {os.path.basename(img)}")
121+
img_pil = Image.open(img)
122+
else:
123+
progress.update(task, description=f"Processing image {i+1}")
124+
img_pil = img
125+
126+
img_b64 = encode_image(img_pil)
127+
processed_images.append(img_b64)
128+
progress.advance(task)
129+
timings["preprocessing"] = t.elapsed_time
130+
131+
# Make server request
132+
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"), console=console) as progress:
133+
task = progress.add_task("Waiting for server response...")
134+
135+
with Timer(enable_print=False) as t:
136+
async with httpx.AsyncClient() as client:
137+
response = await client.post(
138+
self.endpoint,
139+
json={
140+
"images": processed_images,
141+
"prompts": prompts,
142+
"points": points,
143+
"segmentation": segmentation,
144+
"detection": detection,
145+
},
146+
timeout=120.0
147+
)
148+
149+
progress.update(task, completed=True)
150+
result = response.json()
151+
console.print("[green]Server request completed successfully[/green]")
152+
timings["server_request"] = t.elapsed_time
153+
154+
return {"results": result, "timings": timings}
155+
156+
def predict(
157+
self,
158+
images: Union[str, Image.Image, Sequence[Union[str, Image.Image]]],
159+
prompts: Union[str, Sequence[str]],
160+
points: bool = True,
161+
segmentation: bool = False,
162+
detection: bool = False,
163+
) -> Dict:
164+
"""Synchronous wrapper for predict_async."""
165+
with Timer(enable_print=False) as t:
166+
result = asyncio.run(self.predict_async(
167+
images, prompts, points, segmentation, detection
168+
))
169+
result["timings"]["total"] = t.elapsed_time
170+
return result
171+
172+
173+
app = typer.Typer()
174+
175+
176+
@app.command()
177+
def predict(
178+
image: List[str] = typer.Option(
179+
None,
180+
"--image", "-i",
181+
help="Image path(s). Can be specified multiple times for multiple images.",
182+
callback=lambda x: x or [],
183+
),
184+
prompt: List[str] = typer.Option(
185+
None,
186+
"--prompt", "-p",
187+
help="Text prompt(s). Can be specified multiple times for multiple prompts.",
188+
callback=lambda x: x or [],
189+
),
190+
host: str = typer.Option("localhost", help="Host of the PointingGeminiSAM2 service"),
191+
port: int = typer.Option(7100, help="Port of the PointingGeminiSAM2 service"),
192+
points: bool = typer.Option(True, help="Whether to return point coordinates"),
193+
segmentation: bool = typer.Option(False, help="Whether to perform segmentation"),
194+
detection: bool = typer.Option(False, help="Whether to perform detection instead of pointing"),
195+
):
196+
"""Run predictions using the PointingGeminiSAM2 service."""
197+
try:
198+
# Clean up inputs
199+
prompts = [p.strip().strip("\"'").replace('\\"', '"').replace("\\'", "'").replace("\\", "") for p in prompt]
200+
image_paths = [p.strip().strip("\"'").replace('\\"', '"').replace("\\'", "'").replace("\\", "") for p in image]
201+
202+
if not image_paths or not prompts:
203+
raise typer.BadParameter("Must provide at least one image (-i) and one prompt (-p)")
204+
205+
client = PointingGeminiSAM2Client(host=host, port=port)
206+
result = client.predict(image_paths, prompts, points=points, segmentation=segmentation, detection=detection)
207+
208+
# Print results
209+
console.rule("[bold blue]Results")
210+
for res in result["results"]:
211+
img_idx = res["image_index"]
212+
prompt_idx = res["prompt_index"]
213+
prompt = res["prompt"]
214+
215+
console.print(f"\n[yellow]Image {img_idx + 1}, Prompt {prompt_idx + 1}: '{prompt}'[/yellow]")
216+
if res.get("points"):
217+
console.print(f"Points coordinates: {res['points']}", style="cyan")
218+
if res.get("detections"):
219+
console.print("\nBounding boxes:", style="cyan")
220+
for i, box in enumerate(res["detections"], 1):
221+
console.print(f"Box {i}: {box['box_2d']} ({box['label']})", style="cyan")
222+
223+
# Print timing
224+
console.rule("[bold blue]Timing")
225+
console.print(f"Preprocessing: {result['timings']['preprocessing']:.3f}s", style="cyan")
226+
console.print(f"Server request: {result['timings']['server_request']:.3f}s", style="cyan")
227+
console.print(f"Total time: {result['timings']['total']:.3f}s", style="green")
228+
229+
except Exception as e:
230+
console.print(f"\n[red]Error: {str(e)}[/red]")
231+
console.print("\n[yellow]Make sure to:[/yellow]")
232+
console.print("1. Start the server first: [cyan]python -m src.models.segmentation.pointing_gemini_sam2[/cyan]")
233+
console.print("2. Wait a few seconds for the server to initialize")
234+
console.print("3. Try the request again")
235+
236+
237+
if __name__ == "__main__":
238+
app()

0 commit comments

Comments
 (0)