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 ("\n Bounding 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