Skip to content

Commit 9c2db03

Browse files
committed
add utils for gemini pointing
1 parent ad43d77 commit 9c2db03

2 files changed

Lines changed: 295 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Utility functions for parsing Gemini model responses."""
2+
3+
import json
4+
import logging
5+
from typing import Dict, List, Optional, Tuple, Union
6+
7+
from PIL import Image
8+
9+
10+
def parse_gemini_point_response(
11+
response: str,
12+
image_size: Optional[Tuple[int, int]] = None,
13+
normalize: bool = True,
14+
) -> Dict[str, List[Dict[str, Union[List[float], str]]]]:
15+
"""Parse Gemini point response to extract point coordinates and labels.
16+
17+
Args:
18+
response: Raw response from Gemini
19+
image_size: Optional size of the input image (width, height) for normalization
20+
normalize: Whether to normalize points to 0-1000 range
21+
22+
Returns:
23+
Dictionary with points and labels
24+
"""
25+
try:
26+
# Extract JSON from response
27+
start = response.find("[")
28+
end = response.rfind("]") + 1
29+
json_str = response[start:end]
30+
points_data = json.loads(json_str)
31+
32+
# Validate and normalize points
33+
normalized_points = []
34+
for item in points_data:
35+
point = item["point"]
36+
if not isinstance(point, list) or len(point) != 2:
37+
raise ValueError(f"Invalid point format: {point}")
38+
39+
if normalize and image_size:
40+
# Convert to normalized coordinates [0-1000]
41+
y, x = point
42+
y = y * 1000 / image_size[1]
43+
x = x * 1000 / image_size[0]
44+
point = [y, x]
45+
46+
normalized_points.append({
47+
"point": point,
48+
"label": str(item["label"])
49+
})
50+
51+
return {"points": normalized_points}
52+
except (json.JSONDecodeError, KeyError, ValueError) as e:
53+
logging.error(f"Failed to parse Gemini point response: {e}")
54+
logging.error(f"Response: {response}")
55+
raise ValueError("Failed to parse Gemini point response") from e
56+
57+
58+
def parse_gemini_detection_response(
59+
response: str,
60+
image_size: Optional[Tuple[int, int]] = None,
61+
normalize: bool = True,
62+
) -> Dict[str, List[Dict[str, Union[List[float], str]]]]:
63+
"""Parse Gemini detection response to extract bounding boxes and labels.
64+
65+
Args:
66+
response: Raw response from Gemini
67+
image_size: Optional size of the input image (width, height) for normalization
68+
normalize: Whether to normalize boxes to 0-1000 range
69+
70+
Returns:
71+
Dictionary with bounding boxes and labels
72+
"""
73+
try:
74+
# Extract JSON from response
75+
start = response.find("[")
76+
end = response.rfind("]") + 1
77+
json_str = response[start:end]
78+
detection_data = json.loads(json_str)
79+
80+
# Validate and normalize boxes
81+
normalized_boxes = []
82+
for item in detection_data:
83+
box = item["box_2d"]
84+
if not isinstance(box, list) or len(box) != 4:
85+
raise ValueError(f"Invalid box format: {box}")
86+
87+
if normalize and image_size:
88+
# Convert to normalized coordinates [0-1000]
89+
x1, y1, x2, y2 = box
90+
x1 = x1 * 1000 / image_size[0]
91+
y1 = y1 * 1000 / image_size[1]
92+
x2 = x2 * 1000 / image_size[0]
93+
y2 = y2 * 1000 / image_size[1]
94+
box = [x1, y1, x2, y2]
95+
96+
normalized_boxes.append({
97+
"box_2d": box,
98+
"label": str(item["label"])
99+
})
100+
101+
return {"detections": normalized_boxes}
102+
except (json.JSONDecodeError, KeyError, ValueError) as e:
103+
logging.error(f"Failed to parse Gemini detection response: {e}")
104+
logging.error(f"Response: {response}")
105+
raise ValueError("Failed to parse Gemini detection response") from e
106+
107+
108+
def denormalize_point(
109+
point: List[float],
110+
image_size: Tuple[int, int],
111+
) -> List[float]:
112+
"""Convert normalized point [0-1000] back to image coordinates.
113+
114+
Args:
115+
point: Normalized point coordinates [y, x]
116+
image_size: Size of the image (width, height)
117+
118+
Returns:
119+
Point coordinates in image space
120+
"""
121+
y, x = point
122+
y = y * image_size[1] / 1000
123+
x = x * image_size[0] / 1000
124+
return [y, x]
125+
126+
127+
def denormalize_box(
128+
box: List[float],
129+
image_size: Tuple[int, int],
130+
) -> List[float]:
131+
"""Convert normalized box [0-1000] back to image coordinates.
132+
133+
Args:
134+
box: Normalized box coordinates [x1, y1, x2, y2]
135+
image_size: Size of the image (width, height)
136+
137+
Returns:
138+
Box coordinates in image space
139+
"""
140+
x1, y1, x2, y2 = box
141+
x1 = x1 * image_size[0] / 1000
142+
y1 = y1 * image_size[1] / 1000
143+
x2 = x2 * image_size[0] / 1000
144+
y2 = y2 * image_size[1] / 1000
145+
return [x1, y1, x2, y2]
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import os
2+
from typing import Any, List, Optional, Union
3+
4+
import google.generativeai as genai
5+
from PIL import Image
6+
from tenacity import retry, stop_after_attempt, wait_random_exponential
7+
from google.generativeai.generative_models import GenerativeModel
8+
from google.generativeai.types import GenerationConfig
9+
10+
11+
# GEMINI_MODEL_NAME = "gemini-2.5.pro-exp-03-25"
12+
# GEMINI_MODEL_NAME = "gemini-2.5.pro"
13+
GEMINI_MODEL_NAME = "gemini-2.0-flash"
14+
15+
16+
class GeminiClient:
17+
"""Client for interacting with Gemini models."""
18+
19+
def __init__(self, api_key: Optional[str] = None):
20+
"""Initialize the Gemini client.
21+
22+
Args:
23+
api_key: Optional API key. If not provided, will use GEMINI_API_KEY env var.
24+
"""
25+
api_key = api_key or os.getenv("GEMINI_API_KEY")
26+
if not api_key:
27+
raise ValueError(
28+
"GEMINI_API_KEY environment variable must be set or api_key must be provided"
29+
)
30+
genai.configure(api_key=api_key)
31+
32+
def generate(
33+
self,
34+
model: str = GEMINI_MODEL_NAME,
35+
prompt: str = "",
36+
image: Optional[Union[str, Image.Image]] = None,
37+
temperature: float = 0.0,
38+
max_tokens: int = 4096,
39+
**kwargs: Any
40+
) -> str:
41+
"""Generate a response from Gemini.
42+
43+
Args:
44+
model: Name of the Gemini model to use
45+
prompt: Text prompt
46+
image: Optional image (path or PIL Image)
47+
temperature: Sampling temperature
48+
max_tokens: Maximum number of tokens to generate
49+
**kwargs: Additional arguments for the model
50+
51+
Returns:
52+
Generated text response
53+
"""
54+
gen_model = genai.GenerativeModel(model)
55+
56+
if image is None:
57+
response = gen_model.generate_content(
58+
prompt,
59+
generation_config=GenerationConfig(
60+
temperature=temperature,
61+
max_output_tokens=max_tokens,
62+
**kwargs
63+
),
64+
)
65+
else:
66+
if isinstance(image, str):
67+
image = Image.open(image)
68+
response = gen_model.generate_content(
69+
[prompt, image],
70+
generation_config=GenerationConfig(
71+
temperature=temperature,
72+
max_output_tokens=max_tokens,
73+
**kwargs
74+
),
75+
)
76+
77+
return response.text
78+
79+
def generate_stream(
80+
self,
81+
model: str = GEMINI_MODEL_NAME,
82+
prompt: str = "",
83+
image: Optional[Union[str, Image.Image]] = None,
84+
temperature: float = 0.0,
85+
max_tokens: int = 4096,
86+
**kwargs: Any
87+
) -> List[str]:
88+
"""Generate a streaming response from Gemini.
89+
90+
Args:
91+
model: Name of the Gemini model to use
92+
prompt: Text prompt
93+
image: Optional image (path or PIL Image)
94+
temperature: Sampling temperature
95+
max_tokens: Maximum number of tokens to generate
96+
**kwargs: Additional arguments for the model
97+
98+
Returns:
99+
List of text chunks
100+
"""
101+
gen_model = genai.GenerativeModel(model)
102+
103+
if image is None:
104+
response = gen_model.generate_content(
105+
prompt,
106+
stream=True,
107+
generation_config=GenerationConfig(
108+
temperature=temperature,
109+
max_output_tokens=max_tokens,
110+
**kwargs
111+
),
112+
)
113+
else:
114+
if isinstance(image, str):
115+
image = Image.open(image)
116+
response = gen_model.generate_content(
117+
[prompt, image],
118+
stream=True,
119+
generation_config=GenerationConfig(
120+
temperature=temperature,
121+
max_output_tokens=max_tokens,
122+
**kwargs
123+
),
124+
)
125+
126+
return [chunk.text for chunk in response]
127+
128+
129+
@retry(wait=wait_random_exponential(min=1, max=60), stop=stop_after_attempt(6))
130+
def call_google_api(
131+
message: Union[str, List[Union[Any, Image.Image]]],
132+
model: str = "gemini-pro", # gemini-pro, gemini-pro-vision
133+
) -> str:
134+
try:
135+
gen_model = genai.GenerativeModel(model)
136+
response = gen_model.generate_content(message)
137+
response.resolve()
138+
return response.text
139+
except Exception as e:
140+
print(f"{type(e).__name__}: {e}")
141+
raise e
142+
143+
144+
if __name__ == "__main__":
145+
# Example usage
146+
client = GeminiClient()
147+
input = "What color are apples?"
148+
print("input: {}".format(input))
149+
output = call_google_api(input)
150+
print("output: {}".format(output))

0 commit comments

Comments
 (0)