makeitfr commited on
Commit
b258f2f
·
verified ·
1 Parent(s): 382cfb6

Upload OmniParser/eval/ss_pro_gpt4o_omniv2.py with huggingface_hub

Browse files
OmniParser/eval/ss_pro_gpt4o_omniv2.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import ast
4
+ import base64
5
+ from io import BytesIO
6
+ from PIL import Image
7
+ from transformers import AutoModelForCausalLM, AutoTokenizer, GenerationConfig
8
+
9
+ import openai
10
+ from openai import BadRequestError
11
+
12
+ model_name = "gpt-4o-2024-05-13"
13
+ OPENAI_KEY = os.environ.get("OPENAI_API_KEY")
14
+
15
+ def convert_pil_image_to_base64(image):
16
+ buffered = BytesIO()
17
+ image.save(buffered, format="PNG")
18
+ return base64.b64encode(buffered.getvalue()).decode()
19
+
20
+
21
+ from models.utils import get_som_labeled_img, check_ocr_box, get_caption_model_processor, get_yolo_model
22
+ import torch
23
+ from ultralytics import YOLO
24
+ from PIL import Image
25
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
26
+ SOM_MODEL_PATH='...'
27
+ CAPTION_MODEL_PATH='...'
28
+ som_model = get_yolo_model(SOM_MODEL_PATH)
29
+
30
+ som_model.to(device)
31
+ print('model to {}'.format(device))
32
+
33
+ # Caption model removed - using OCR-only mode
34
+ import importlib
35
+ caption_model_processor = None
36
+ # For caption generation, use a custom caption model via config
37
+
38
+ def omniparser_parse(image, image_path):
39
+ box_overlay_ratio = max(image.size) / 3200
40
+ draw_bbox_config = {
41
+ 'text_scale': 0.8 * box_overlay_ratio,
42
+ 'text_thickness': max(int(2 * box_overlay_ratio), 1),
43
+ 'text_padding': max(int(3 * box_overlay_ratio), 1),
44
+ 'thickness': max(int(3 * box_overlay_ratio), 1),
45
+ }
46
+ BOX_TRESHOLD = 0.05
47
+
48
+ ocr_bbox_rslt, is_goal_filtered = check_ocr_box(image_path, display_img = False, output_bb_format='xyxy', goal_filtering=None, easyocr_args={'paragraph': False, 'text_threshold':0.5, 'canvas_size':max(image.size), 'decoder':'beamsearch', 'beamWidth':10, 'batch_size':256}, use_paddleocr=False)
49
+ text, ocr_bbox = ocr_bbox_rslt
50
+
51
+ dino_labled_img, label_coordinates, parsed_content_list = get_som_labeled_img(image_path, som_model, BOX_TRESHOLD = BOX_TRESHOLD, output_coord_in_ratio=True, ocr_bbox=ocr_bbox,draw_bbox_config=draw_bbox_config, caption_model_processor=caption_model_processor, ocr_text=text,use_local_semantics=True, iou_threshold=0.7, scale_img=False, batch_size=128)
52
+ return dino_labled_img, label_coordinates, parsed_content_list
53
+
54
+ def reformat_messages(parsed_content_list):
55
+ screen_info = ""
56
+ for idx, element in enumerate(parsed_content_list):
57
+ element['idx'] = idx
58
+ if element['type'] == 'text':
59
+ screen_info += f'''<p id={idx} class="text" alt="{element['content']}"> </p>\n'''
60
+ # screen_info += f'ID: {idx}, Text: {element["content"]}\n'
61
+ elif element['type'] == 'icon':
62
+ screen_info += f'''<img id={idx} class="icon" alt="{element['content']}"> </img>\n'''
63
+ # screen_info += f'ID: {idx}, Icon: {element["content"]}\n'
64
+ return screen_info
65
+
66
+ PROMPT_TEMPLATE_SEECLICK_PARSED_CONTENT = '''Please generate the next move according to the UI screenshot and task instruction. You will be presented with a screenshot image. Also you will be given each bounding box's description in a list. To complete the task, You should choose a related bbox to click based on the bbox descriptions.
67
+ Task instruction: {}.
68
+ Here is the list of all detected bounding boxes by IDs and their descriptions: {}. Keep in mind the description for Text Boxes are likely more accurate than the description for Icon Boxes.
69
+ Requirement: 1. You should first give a reasonable description of the current screenshot, and give a short analysis of how can the user task be achieved. 2. Then make an educated guess of bbox id to click in order to complete the task based on the bounding boxes descriptions. 3. Your answer should follow the following format: {{"Analysis": xxx, "Click BBox ID": "y"}}. Do not include any other info. Some examples: {}. The task is to {}. Retrieve the bbox id where its description matches the task instruction. Now start your answer:'''
70
+
71
+ # PROMPT_TEMPLATE_SEECLICK_PARSED_CONTENT_v1 = "The instruction is to {}. \nHere is the list of all detected bounding boxes by IDs and their descriptions: {}. \nKeep in mind the description for Text Boxes are likely more accurate than the description for Icon Boxes. \n Requirement: 1. You should first give a reasonable description of the current screenshot, and give a step by step analysis of how can the user task be achieved. 2. Then make an educated guess of bbox id to click in order to complete the task using both the visual information from the screenshot image and the bounding boxes descriptions. 3. Your answer should follow the following format: {{'Analysis': 'xxx', 'Click BBox ID': 'y'}}. Please do not include any other info."
72
+ PROMPT_TEMPLATE_SEECLICK_PARSED_CONTENT_v1 = "The instruction is to {}. \nHere is the list of all detected bounding boxes by IDs and their descriptions: {}. \nKeep in mind the description for Text Boxes are likely more accurate than the description for Icon Boxes. \n Requirement: 1. You should first give a reasonable description of the current screenshot, and give a some analysis of how can the user instruction be achieved by a single click. 2. Then make an educated guess of bbox id to click in order to complete the task using both the visual information from the screenshot image and the bounding boxes descriptions. REMEMBER: the task instruction must be achieved by one single click. 3. Your answer should follow the following format: {{'Analysis': 'xxx', 'Click BBox ID': 'y'}}. Please do not include any other info."
73
+
74
+
75
+ FEWSHOT_EXAMPLE = '''Example 1: Task instruction: Next page. \n{"Analysis": "Based on the screenshot and icon descriptions, I should click on the next page icon, which is labeled with box ID x in the bounding box list", "Click BBox ID": "x"}\n\n
76
+ Example 2: Task instruction: Search on google. \n{"Analysis": "Based on the screenshot and icon descriptions, I should click on the 'Search' box, which is labeled with box ID y in the bounding box list", "Click BBox ID": "y"}'''
77
+
78
+
79
+
80
+
81
+ from azure.identity import AzureCliCredential, DefaultAzureCredential, get_bearer_token_provider
82
+ from openai import AzureOpenAI
83
+ from models.utils import get_pred_phi3v, extract_dict_from_text, get_phi3v_model_dict
84
+
85
+ class GPT4XModel():
86
+ def __init__(self, model_name="gpt-4o-2024-05-13", use_managed_identity=False):
87
+ self.client = openai.OpenAI(
88
+ api_key=OPENAI_KEY,
89
+ )
90
+ self.model_name = model_name
91
+ if model_name == 'phi35v':
92
+ self.model_dict = get_phi3v_model_dict()
93
+
94
+ def load_model(self):
95
+ pass
96
+
97
+ def set_generation_config(self, **kwargs):
98
+ self.override_generation_config.update(kwargs)
99
+
100
+ def ground_only_positive_phi35v(self, instruction, image):
101
+ if isinstance(image, str):
102
+ image_path = image
103
+ assert os.path.exists(image_path) and os.path.isfile(image_path), "Invalid input image path."
104
+ image = Image.open(image_path).convert('RGB')
105
+ assert isinstance(image, Image.Image), "Invalid input image."
106
+
107
+ base64_image = convert_pil_image_to_base64(image)
108
+ dino_labled_img, label_coordinates, parsed_content_list = omniparser_parse(image, image_path)
109
+ screen_info = reformat_messages(parsed_content_list)
110
+ prompt_origin = PROMPT_TEMPLATE_SEECLICK_PARSED_CONTENT.format(instruction, screen_info, FEWSHOT_EXAMPLE, instruction)
111
+ # prompt_origin = PROMPT_TEMPLATE_SEECLICK_PARSED_CONTENT_v1.format(instruction, screen_info)
112
+
113
+ # Use the get_pred_phi3v function to get predictions
114
+ icon_id, bbox, click_point, response_text = get_pred_phi3v(prompt_origin, (base64_image, dino_labled_img), label_coordinates, id_key='Click ID', model_dict=self.model_dict)
115
+
116
+ result_dict = {
117
+ "result": "positive",
118
+ "bbox": bbox,
119
+ "point": click_point,
120
+ "raw_response": response_text,
121
+ 'dino_labled_img': dino_labled_img,
122
+ 'screen_info': screen_info,
123
+ }
124
+
125
+ return result_dict
126
+
127
+ def ground_only_positive(self, instruction, image):
128
+ if isinstance(image, str):
129
+ image_path = image
130
+ assert os.path.exists(image_path) and os.path.isfile(image_path), "Invalid input image path."
131
+ image = Image.open(image_path).convert('RGB')
132
+ assert isinstance(image, Image.Image), "Invalid input image."
133
+
134
+ base64_image = convert_pil_image_to_base64(image)
135
+ dino_labled_img, label_coordinates, parsed_content_list = omniparser_parse(image, image_path)
136
+ screen_info = reformat_messages(parsed_content_list)
137
+ # prompt_origin = PROMPT_TEMPLATE_SEECLICK_PARSED_CONTENT.format(screen_info, FEWSHOT_EXAMPLE, instruction)
138
+ prompt_origin = PROMPT_TEMPLATE_SEECLICK_PARSED_CONTENT_v1.format(instruction, screen_info)
139
+
140
+ try:
141
+ response = self.client.chat.completions.create(
142
+ model=self.model_name,
143
+ messages=[
144
+ {
145
+ "role": "system",
146
+ "content": [
147
+ # {"type": "text", "text": "You are an expert in using electronic devices and interacting with graphic interfaces. You should not call any external tools."}
148
+ {"type": "text", "text": '''You are an expert at completing instructions on GUI screens.
149
+ You will be presented with two images. The first is the original screenshot. The second is the same screenshot with some numeric tags. You will also be provided with some descriptions of the bbox, and your task is to choose the numeric bbox idx you want to click in order to complete the user instruction.'''}
150
+ ],
151
+ },
152
+ {
153
+ "role": "user",
154
+ "content": [
155
+ {
156
+ "type": "text",
157
+ "text": prompt_origin
158
+
159
+ },
160
+ {
161
+ "type": "image_url",
162
+ "image_url": {
163
+ "url": f"data:image/png;base64,{base64_image}",
164
+ }
165
+ },
166
+ {
167
+ "type": "image_url",
168
+ "image_url": {
169
+ "url": f"data:image/png;base64,{dino_labled_img}",
170
+ }
171
+ },
172
+ ],
173
+ }
174
+ ],
175
+ temperature=self.override_generation_config['temperature'],
176
+ max_tokens=2048,
177
+ )
178
+ response_text = response.choices[0].message.content
179
+ except BadRequestError as e:
180
+ print("OpenAI BadRequestError:", e)
181
+ return None
182
+
183
+ # Extract bounding box
184
+ # print("------")
185
+ # print(grounding_prompt)
186
+ print("------")
187
+ print(response_text)
188
+ # print("------")
189
+ # Try getting groundings
190
+ # bbox = extract_first_bounding_box(response_text)
191
+ # click_point = extract_first_point(response_text)
192
+
193
+ # if not click_point and bbox:
194
+ # click_point = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
195
+ response_text = response_text.replace('```json', '').replace('```', '') #TODO: fix this
196
+
197
+ try:
198
+ response_text = ast.literal_eval(response_text)
199
+
200
+ icon_id = response_text['Click BBox ID']
201
+ bbox = label_coordinates[str(icon_id)]
202
+ click_point = [bbox[0] + bbox[2]/2, bbox[1] + bbox[3]/2]
203
+ except:
204
+ print('error parsing, use regex to parse!!!')
205
+ response_text = extract_dict_from_text(response_text)
206
+ icon_id = response_text['Click BBox ID']
207
+ bbox = label_coordinates[str(icon_id)]
208
+ click_point = [bbox[0] + bbox[2]/2, bbox[1] + bbox[3]/2]
209
+
210
+ result_dict = {
211
+ "result": "positive",
212
+ "bbox": bbox,
213
+ "point": click_point,
214
+ "raw_response": response_text,
215
+ 'dino_labled_img': dino_labled_img,
216
+ 'screen_info': screen_info,
217
+ }
218
+
219
+ return result_dict
220
+
221
+ def ground_allow_negative(self, instruction, image=None):
222
+ if isinstance(image, str):
223
+ image_path = image
224
+ assert os.path.exists(image_path) and os.path.isfile(image_path), "Invalid input image path."
225
+ image = Image.open(image_path).convert('RGB')
226
+ assert isinstance(image, Image.Image), "Invalid input image."
227
+
228
+ base64_image = convert_pil_image_to_base64(image)
229
+
230
+ try:
231
+ response = self.client.chat.completions.create(
232
+ model=self.model_name,
233
+ messages=[
234
+ {
235
+ "role": "system",
236
+ "content": [
237
+ {"type": "text", "text": "You are an expert in using electronic devices and interacting with graphic interfaces. You should not call any external tools."}
238
+ ],
239
+ },
240
+ {
241
+ "role": "user",
242
+ "content": [
243
+ {
244
+ "type": "image_url",
245
+ "image_url": {
246
+ "url": f"data:image/png;base64,{base64_image}",
247
+ }
248
+ },
249
+ {
250
+ "type": "text",
251
+ "text": "You are asked to find the bounding box of an UI element in the given screenshot corresponding to a given instruction.\n"
252
+ "Don't output any analysis. Output your result in the format of [[x0,y0,x1,y1]], with x and y ranging from 0 to 1. \n"
253
+ "If such element does not exist, output only the text 'Target not existent'.\n"
254
+ "The instruction is:\n"
255
+ f"{instruction}\n"
256
+ }
257
+ ],
258
+ }
259
+ ],
260
+ temperature=self.override_generation_config['temperature'],
261
+ max_tokens=2048,
262
+ )
263
+ response_text = response.choices[0].message.content
264
+ except BadRequestError as e:
265
+ print("OpenAI BadRequestError:", e)
266
+ return {
267
+ "result": "failed"
268
+ }
269
+
270
+ # Extract bounding box
271
+ # print("------")
272
+ # print(grounding_prompt)
273
+ print("------")
274
+ print(response_text)
275
+ # print("------")
276
+
277
+ if "not existent" in response_text.lower():
278
+ return {
279
+ "result": "negative",
280
+ "bbox": None,
281
+ "point": None,
282
+ "raw_response": response_text
283
+ }
284
+
285
+ # Try getting groundings
286
+ bbox = extract_first_bounding_box(response_text)
287
+ click_point = extract_first_point(response_text)
288
+
289
+ if not click_point and bbox:
290
+ click_point = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
291
+
292
+ result_dict = {
293
+ "result": "positive" if bbox or click_point else "negative",
294
+ "bbox": bbox,
295
+ "point": click_point,
296
+ "raw_response": response_text
297
+ }
298
+
299
+ return result_dict
300
+
301
+
302
+ def ground_with_uncertainty(self, instruction, image=None):
303
+ if isinstance(image, str):
304
+ image_path = image
305
+ assert os.path.exists(image_path) and os.path.isfile(image_path), "Invalid input image path."
306
+ image = Image.open(image_path).convert('RGB')
307
+ assert isinstance(image, Image.Image), "Invalid input image."
308
+
309
+ base64_image = convert_pil_image_to_base64(image)
310
+
311
+ try:
312
+ response = self.client.chat.completions.create(
313
+ model=self.model_name,
314
+ messages=[
315
+ {
316
+ "role": "system",
317
+ "content": [
318
+ {"type": "text", "text": "You are an expert in using electronic devices and interacting with graphic interfaces. You should not call any external tools."}
319
+ ],
320
+ },
321
+ {
322
+ "role": "user",
323
+ "content": [
324
+ {
325
+ "type": "image_url",
326
+ "image_url": {
327
+ "url": f"data:image/png;base64,{base64_image}",
328
+ }
329
+ },
330
+ {
331
+ "type": "text",
332
+ "text": "You are asked to find the bounding box of an UI element in the given screenshot corresponding to a given instruction.\n"
333
+ "- If such element does not exist in the screenshot, output only the text 'Target not existent'."
334
+
335
+ "- If you are sure such element exists and you are confident in finding it, output your result in the format of [[x0,y0,x1,y1]], with x and y ranging from 0 to 1. \n"
336
+ "Please find out the bounding box of the UI element corresponding to the following instruction: \n"
337
+ "The instruction is:\n"
338
+ f"{instruction}\n"
339
+
340
+ }
341
+ ],
342
+ }
343
+ ],
344
+ temperature=self.override_generation_config['temperature'],
345
+ max_tokens=2048,
346
+ )
347
+ response_text = response.choices[0].message.content
348
+ except BadRequestError as e:
349
+ print("OpenAI BadRequestError:", e)
350
+ return {
351
+ "result": "failed"
352
+ }
353
+
354
+ # Extract bounding box
355
+ # print("------")
356
+ # print(grounding_prompt)
357
+ print("------")
358
+ print(response_text)
359
+ # print("------")
360
+
361
+ if "not found" in response_text.lower():
362
+ return {
363
+ "result": "negative",
364
+ "bbox": None,
365
+ "point": None,
366
+ "raw_response": response_text
367
+ }
368
+
369
+ # Try getting groundings
370
+ bbox = extract_first_bounding_box(response_text)
371
+ click_point = extract_first_point(response_text)
372
+
373
+ if not click_point and bbox:
374
+ click_point = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]
375
+
376
+ result_dict = {
377
+ "result": "positive",
378
+ "bbox": bbox,
379
+ "point": click_point,
380
+ "raw_response": response_text
381
+ }
382
+
383
+ return result_dict
384
+
385
+ def extract_first_bounding_box(text):
386
+ # Regular expression pattern to match the first bounding box in the format [[x0,y0,x1,y1]]
387
+ # This captures the entire float value using \d for digits and optional decimal points
388
+ pattern = r"\[\[(\d+\.\d+|\d+),(\d+\.\d+|\d+),(\d+\.\d+|\d+),(\d+\.\d+|\d+)\]\]"
389
+
390
+ # Search for the first match in the text
391
+ match = re.search(pattern, text, re.DOTALL)
392
+
393
+ if match:
394
+ # Capture the bounding box coordinates as floats
395
+ bbox = [float(match.group(1)), float(match.group(2)), float(match.group(3)), float(match.group(4))]
396
+ return bbox
397
+ return None
398
+
399
+
400
+ def extract_first_point(text):
401
+ # Regular expression pattern to match the first point in the format [[x0,y0]]
402
+ # This captures the entire float value using \d for digits and optional decimal points
403
+ pattern = r"\[\[(\d+\.\d+|\d+),(\d+\.\d+|\d+)\]\]"
404
+
405
+ # Search for the first match in the text
406
+ match = re.search(pattern, text, re.DOTALL)
407
+
408
+ if match:
409
+ point = [float(match.group(1)), float(match.group(2))]
410
+ return point
411
+
412
+ return None