From 4f18c4b4b5765b63d0f5fe765577c0f1d411ce34 Mon Sep 17 00:00:00 2001 From: kithib <1010465183@qq.com> Date: Thu, 18 Apr 2024 15:36:55 +0800 Subject: [PATCH 01/15] Merge remote-tracking branch 'origin/main' --- .../android/GroundingDINO_SwinT_OGC.py | 43 +++++++ .../environment/android/android_ext_env.py | 111 +++++++++++++++--- .../android/text_icon_localization.py | 67 +++++------ metagpt/utils/download_modelweight.py | 22 ++++ requirements.txt | 22 +++- 5 files changed, 207 insertions(+), 58 deletions(-) create mode 100644 metagpt/environment/android/GroundingDINO_SwinT_OGC.py create mode 100644 metagpt/utils/download_modelweight.py diff --git a/metagpt/environment/android/GroundingDINO_SwinT_OGC.py b/metagpt/environment/android/GroundingDINO_SwinT_OGC.py new file mode 100644 index 000000000..9158d5f62 --- /dev/null +++ b/metagpt/environment/android/GroundingDINO_SwinT_OGC.py @@ -0,0 +1,43 @@ +batch_size = 1 +modelname = "groundingdino" +backbone = "swin_T_224_1k" +position_embedding = "sine" +pe_temperatureH = 20 +pe_temperatureW = 20 +return_interm_indices = [1, 2, 3] +backbone_freeze_keywords = None +enc_layers = 6 +dec_layers = 6 +pre_norm = False +dim_feedforward = 2048 +hidden_dim = 256 +dropout = 0.0 +nheads = 8 +num_queries = 900 +query_dim = 4 +num_patterns = 0 +num_feature_levels = 4 +enc_n_points = 4 +dec_n_points = 4 +two_stage_type = "standard" +two_stage_bbox_embed_share = False +two_stage_class_embed_share = False +transformer_activation = "relu" +dec_pred_bbox_embed_share = True +dn_box_noise_scale = 1.0 +dn_label_noise_ratio = 0.5 +dn_label_coef = 1.0 +dn_bbox_coef = 1.0 +embed_init_tgt = True +dn_labelbook_size = 2000 +max_text_len = 256 +text_encoder_type = "bert-base-uncased" +use_text_enhancer = True +use_fusion_layer = True +use_checkpoint = True +use_transformer_ckpt = True +use_text_cross_attention = True +text_dropout = 0.0 +fusion_dropout = 0.0 +fusion_droppath = 0.1 +sub_sentence_present = True diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index 152c71d04..230a351ad 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -1,17 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Desc : The Android external environment to integrate with Android apps - +import os import subprocess +import clip +import time from pathlib import Path from typing import Any, Optional from modelscope.pipelines import pipeline from modelscope.utils.constant import Tasks + from PIL import Image from pydantic import Field -from metagpt.environment.android.text_icon_localization import ocr +from metagpt.environment.android.text_icon_localization import * from metagpt.environment.android.const import ADB_EXEC_FAIL from metagpt.environment.android.env_space import ( EnvAction, @@ -22,6 +25,7 @@ from metagpt.environment.android.env_space import ( ) from metagpt.environment.base_env import ExtEnv, mark_as_readable, mark_as_writeable from metagpt.logs import logger +from metagpt.utils.download_modelweight import download_model class AndroidExtEnv(ExtEnv): @@ -42,14 +46,14 @@ class AndroidExtEnv(ExtEnv): self.width = data.get("width", width) self.height = data.get("height", height) - self.create_device_path(self.screenshot_dir) - self.create_device_path(self.xml_dir) + # self.create_device_path(self.screenshot_dir) + # self.create_device_path(self.xml_dir) def reset( - self, - *, - seed: Optional[int] = None, - options: Optional[dict[str, Any]] = None, + self, + *, + seed: Optional[int] = None, + options: Optional[dict[str, Any]] = None, ) -> tuple[dict[str, Any], dict[str, Any]]: super().reset(seed=seed, options=options) @@ -154,14 +158,17 @@ class AndroidExtEnv(ExtEnv): ss_remote_path = Path(self.screenshot_dir).joinpath(f"{ss_name}.png") ss_cmd = f"{self.adb_prefix_shell} screencap -p {ss_remote_path}" ss_res = self.execute_adb_with_cmd(ss_cmd) - + time.sleep(0.1) res = ADB_EXEC_FAIL if ss_res != ADB_EXEC_FAIL: ss_local_path = Path(local_save_dir).joinpath(f"{ss_name}.png") pull_cmd = f"{self.adb_prefix} pull {ss_remote_path} {ss_local_path}" pull_res = self.execute_adb_with_cmd(pull_cmd) + time.sleep(0.1) if pull_res != ADB_EXEC_FAIL: res = ss_local_path + else: + res = get_screenshot_only(local_save_dir) return Path(res) @mark_as_readable @@ -229,22 +236,22 @@ class AndroidExtEnv(ExtEnv): return swipe_res @mark_as_writeable - def user_swipe_to(self, start: tuple[int, int], end: tuple[int, int], duration: int = 400): + def user_swipe_to(self, start: tuple[int, int], end: tuple[int, int], duration: int = 400) -> str: adb_cmd = f"{self.adb_prefix_si} swipe {start[0]} {start[1]} {end[0]} {end[1]} {duration}" swipe_res = self.execute_adb_with_cmd(adb_cmd) return swipe_res @mark_as_writeable - def user_exit(self): - adb_cmd = "adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME" + def user_exit(self) -> str: + adb_cmd = f"{self.adb_prefix_shell} am start -a android.intent.action.MAIN -c android.intent.category.HOME" exit_res = self.execute_adb_with_cmd(adb_cmd) return exit_res @mark_as_writeable - def user_openApp(self, app_name: str): - # openApp without xml - screenshot_path = self.get_screenshot("screenshot", "../../../examples/data/screenshot") - image = screenshot_path + def _ocr_text(self, text: str) -> list: + if not os.path.exists(self.screenshot_dir): + os.makedirs(self.screenshot_dir) + image = self.get_screenshot("screenshot", self.screenshot_dir) ocr_detection = pipeline(Tasks.ocr_detection, model="damo/cv_resnet18_ocr-detection-line-level_damo") ocr_recognition = pipeline(Tasks.ocr_recognition, model="damo/cv_convnextTiny_ocr-recognition-document_damo") iw, ih = Image.open(image).size @@ -252,7 +259,15 @@ class AndroidExtEnv(ExtEnv): if iw > ih: x, y = y, x iw, ih = ih, iw - in_coordinate, out_coordinate = ocr(image, app_name, ocr_detection, ocr_recognition, iw, ih) + in_coordinate, out_coordinate = ocr(image, text, ocr_detection, ocr_recognition, iw, ih) + output_list = [in_coordinate, out_coordinate, x, y, iw, ih, image] + return output_list + + @mark_as_writeable + def user_open_app(self, app_name: str) -> str: + ocr_result = self._ocr_text(app_name) + in_coordinate, out_coordinate, x, y, iw, ih = ( + ocr_result[0], ocr_result[1], ocr_result[2], ocr_result[3], ocr_result[4], ocr_result[5]) if len(in_coordinate) == 0: logger.info(f"No App named {app_name}.") return "no" @@ -262,11 +277,69 @@ class AndroidExtEnv(ExtEnv): (in_coordinate[0][1] + in_coordinate[0][3]) / 2, ] tap_coordinate = [round(tap_coordinate[0] / iw, 2), round(tap_coordinate[1] / ih, 2)] - #print(f"{parameter}在屏幕的坐标为为{tap_coordinate[0] * x} ,{(tap_coordinate[1] - round(50 / y, 2)) * y}") return self.system_tap(tap_coordinate[0] * x, (tap_coordinate[1] - round(50 / y, 2)) * y) + @mark_as_writeable + def user_click_text(self, text: str) -> str: + ocr_result = self._ocr_text(text) + in_coordinate, out_coordinate, x, y, iw, ih, image = ( + ocr_result[0], ocr_result[1], ocr_result[2], ocr_result[3], ocr_result[4], ocr_result[5], ocr_result[6]) + if len(out_coordinate) == 0: + logger.info( + f"Failed to execute action click text ({text}). The text \"{text}\" is not detected in the screenshot.") + elif len(out_coordinate) == 1: + tap_coordinate = [(in_coordinate[0][0] + in_coordinate[0][2]) / 2, + (in_coordinate[0][1] + in_coordinate[0][3]) / 2] + tap_coordinate = [round(tap_coordinate[0] / iw, 2), round(tap_coordinate[1] / ih, 2)] + return self.system_tap(tap_coordinate[0] * x, tap_coordinate[1] * y) + else: + logger.info( + f"Failed to execute action click text ({text}). There are too many text \"{text}\" in the screenshot.") + @mark_as_writeable def user_stop(self): logger.info("Successful execution of tasks") - # todo : user_clickIcon + @mark_as_writeable + def user_click_icon(self, icon_shape_color: str) -> str: + if not os.path.exists(self.screenshot_dir): + os.makedirs(self.screenshot_dir) + screenshot_path = self.get_screenshot("screenshot", self.screenshot_dir) + image, device = screenshot_path, 'cpu' + iw, ih = Image.open(image).size + x, y = self.device_shape + if iw > ih: + x, y = y, x + iw, ih = ih, iw + # 下载权重文件 + file_url = 'https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth' # 加载远程model + target_folder = '/Users/kit/Desktop/深度赋值/amzingproject/MetaGPT/workspace/weights' + file_path = download_model(file_url, target_folder) + groundingdino_model = load_model(file_path, device=device).eval() + in_coordinate, out_coordinate = det(image, "icon", groundingdino_model) # 检测icon + if len(out_coordinate) == 1: # only one icon + tap_coordinate = [(in_coordinate[0][0] + in_coordinate[0][2]) / 2, + (in_coordinate[0][1] + in_coordinate[0][3]) / 2] + tap_coordinate = [round(tap_coordinate[0] / iw, 2), round(tap_coordinate[1] / ih, 2)] + return self.system_tap(tap_coordinate[0] * x, tap_coordinate[1] * y) + + else: + temp_file = "/Users/kit/Desktop/深度赋值/amzingproject/MetaGPT/workspace/temp" + if not os.path.exists(temp_file): + os.mkdir(temp_file) + hash_table, clip_filter= [],[] + for i, (td, box) in enumerate(zip(in_coordinate, out_coordinate)): + if crop_for_clip(image, td, i, temp_file): + hash_table.append(td) + crop_image = f"{i}.jpg" + clip_filter.append(os.path.join(temp_file, crop_image)) + clip_model, clip_preprocess = clip.load("ViT-B/32", device=device) + clip_filter = clip_for_icon(clip_model, clip_preprocess, clip_filter, icon_shape_color) + final_box = hash_table[clip_filter] + tap_coordinate = [(final_box[0] + final_box[2]) / 2, (final_box[1] + final_box[3]) / 2] + tap_coordinate = [round(tap_coordinate[0] / iw, 2), round(tap_coordinate[1] / ih, 2)] + return self.system_tap(tap_coordinate[0] * x, tap_coordinate[1] * y) + + + + diff --git a/metagpt/environment/android/text_icon_localization.py b/metagpt/environment/android/text_icon_localization.py index d1b5ba2f9..8c3d22c7c 100644 --- a/metagpt/environment/android/text_icon_localization.py +++ b/metagpt/environment/android/text_icon_localization.py @@ -3,7 +3,9 @@ import clip import cv2 import numpy as np import torch - +import subprocess +import time +from pathlib import Path import groundingdino.datasets.transforms as T from groundingdino.models import build_model from groundingdino.util.slconfig import SLConfig @@ -12,6 +14,7 @@ from PIL import Image, ImageDraw ################################## text_localization using ocr ####################### + def crop_image(img, position): def distance(x1, y1, x2, y2): return math.sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2)) @@ -96,7 +99,7 @@ def ocr(image_path, prompt, ocr_detection, ocr_recognition, x, y): image = Image.open(image_path) iw, ih = image.size - image_full = cv2.imread(image_path) + image_full = cv2.imread(str(image_path)) det_result = ocr_detection(image_full) det_result = det_result["polygons"] for i in range(det_result.shape[0]): @@ -205,7 +208,6 @@ def calculate_iou(box1, box2): def crop(image, box, i, text_data=None): image = Image.open(image) - if text_data: draw = ImageDraw.Draw(image) draw.rectangle(((text_data[0], text_data[1]), (text_data[2], text_data[3])), outline="red", width=5) @@ -224,31 +226,13 @@ def in_box(box, target): return False -def crop_for_clip(image, box, i, position): +def crop_for_clip(image, box, i, temp_file): image = Image.open(image) w, h = image.size - if position == "left": - bound = [0, 0, w / 2, h] - elif position == "right": - bound = [w / 2, 0, w, h] - elif position == "top": - bound = [0, 0, w, h / 2] - elif position == "bottom": - bound = [0, h / 2, w, h] - elif position == "top left": - bound = [0, 0, w / 2, h / 2] - elif position == "top right": - bound = [w / 2, 0, w, h / 2] - elif position == "bottom left": - bound = [0, h / 2, w / 2, h] - elif position == "bottom right": - bound = [w / 2, h / 2, w, h] - else: - bound = [0, 0, w, h] - + bound = [0, 0, w, h] if in_box(box, bound): cropped_image = image.crop(box) - cropped_image.save(f"./temp/{i}.jpg") + cropped_image.save(f"{temp_file}/{i}.jpg") return True else: return False @@ -286,7 +270,8 @@ def transform_image(image_pil): return image -def load_model(model_config_path, model_checkpoint_path, device): +def load_model(model_checkpoint_path, device): + model_config_path = 'GroundingDINO_SwinT_OGC.py' args = SLConfig.fromfile(model_config_path) args.device = device model = build_model(args) @@ -387,17 +372,23 @@ def det(input_image, text_prompt, groundingdino_model, box_threshold=0.05, text_ return image_data, coordinate -if __name__ == "__main__": - from modelscope.pipelines import pipeline - from modelscope.utils.constant import Tasks +def get_screenshot_only(screenshot_dir: Path) -> str: + command = " adb shell rm /sdcard/screenshot.png" + subprocess.run(command, capture_output=True, text=True, shell=True) + time.sleep(0.1) + command = "adb shell screencap -p /sdcard/screenshot.png" + subprocess.run(command, capture_output=True, text=True, shell=True) + time.sleep(0.1) + command = f"adb pull /sdcard/screenshot.png {screenshot_dir}" + subprocess.run(command, capture_output=True, text=True, shell=True) + image_path = f"{screenshot_dir}/screenshot.png" + save_path = f"{screenshot_dir}/screenshot.jpg" + image = Image.open(image_path) + original_width, original_height = image.size + new_width = int(original_width * 0.5) + new_height = int(original_height * 0.5) + resized_image = image.resize((new_width, new_height)) + resized_image.convert("RGB").save(save_path, "JPEG") + time.sleep(0.1) + return save_path - image_ori = "./screenshot/screenshot.png" - image = "./screenshot/screenshot.png" - parameter = "抖音" - ocr_detection = pipeline(Tasks.ocr_detection, model="damo/cv_resnet18_ocr-detection-line-level_damo") - ocr_recognition = pipeline(Tasks.ocr_recognition, model="damo/cv_convnextTiny_ocr-recognition-document_damo") - iw, ih = Image.open(image).size - if iw > ih: - iw, ih = ih, iw - in_coordinate, out_coordinate = ocr(image_ori, parameter, ocr_detection, ocr_recognition, iw, ih) - print(f"ocr 计算结果为 {in_coordinate} ,{out_coordinate} ") diff --git a/metagpt/utils/download_modelweight.py b/metagpt/utils/download_modelweight.py new file mode 100644 index 000000000..2b8bcf41b --- /dev/null +++ b/metagpt/utils/download_modelweight.py @@ -0,0 +1,22 @@ +import os +import requests +from pathlib import Path + + +def download_model(file_url: str, target_folder: str) -> str: + file_name = file_url.split('/')[-1] # 文件名(从URL中提取) + file_path = os.path.join(target_folder, file_name) # 完整的文件路径 + if not os.path.exists(target_folder): + os.makedirs(target_folder) + # 发起GET请求下载文件 + try: + response = requests.get(file_url, stream=True) + response.raise_for_status() # 检查请求是否成功 + # 保存文件 + with open(file_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + print(f'权重文件已下载并保存至 {file_path}') + except requests.exceptions.HTTPError as err: + print(f'权重文件下载过程中发生错误: {err}') + return file_path diff --git a/requirements.txt b/requirements.txt index d150d61f3..46832e943 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,4 +70,24 @@ qianfan==0.3.2 dashscope==1.14.1 rank-bm25==0.2.2 # for tool recommendation jieba==0.42.1 # for tool recommendation -gymnasium==0.29.1 \ No newline at end of file +gymnasium==0.29.1 + +# for clip and ocr +git+https://github.com/openai/CLIP.git +protobuf<3.20,>=3.9.2 +modelscope +tensorflow==2.9.1; os_name == 'linux' +tensorflow-macos==2.9; os_name == 'darwin' +keras==2.9.0 +torch +torchvision +transformers +opencv-python +matplotlib +pycocotools +timm +SentencePiece +tf_slim +tf_keras +pyclipper +shapely \ No newline at end of file From 4dc00a4fc1d3c870b060643ff1f4a72f48655344 Mon Sep 17 00:00:00 2001 From: kithib <1010465183@qq.com> Date: Thu, 18 Apr 2024 15:41:52 +0800 Subject: [PATCH 02/15] Merge remote-tracking branch 'origin/main' --- examples/data/screenshot/screenshot.jpg | Bin 52748 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/data/screenshot/screenshot.jpg diff --git a/examples/data/screenshot/screenshot.jpg b/examples/data/screenshot/screenshot.jpg deleted file mode 100644 index bb8ba56e2c0654ab6c5149d720462fdc61160b5c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52748 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 B?u-Bc From 24fa32b39520c659b19372ace539c20ddaa137f3 Mon Sep 17 00:00:00 2001 From: kithib <1010465183@qq.com> Date: Thu, 18 Apr 2024 15:43:59 +0800 Subject: [PATCH 03/15] Merge remote-tracking branch 'origin/main' --- metagpt/environment/android/android_ext_env.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index 230a351ad..0c616cafc 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -46,14 +46,14 @@ class AndroidExtEnv(ExtEnv): self.width = data.get("width", width) self.height = data.get("height", height) - # self.create_device_path(self.screenshot_dir) - # self.create_device_path(self.xml_dir) + self.create_device_path(self.screenshot_dir) + self.create_device_path(self.xml_dir) def reset( - self, - *, - seed: Optional[int] = None, - options: Optional[dict[str, Any]] = None, + self, + *, + seed: Optional[int] = None, + options: Optional[dict[str, Any]] = None, ) -> tuple[dict[str, Any], dict[str, Any]]: super().reset(seed=seed, options=options) From d2e461a1e85b109d2fdc2a5272d8f033a30b4c09 Mon Sep 17 00:00:00 2001 From: kithib <1010465183@qq.com> Date: Thu, 18 Apr 2024 15:45:25 +0800 Subject: [PATCH 04/15] Merge remote-tracking branch 'origin/main' --- metagpt/environment/android/android_ext_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index 0c616cafc..eb8a69330 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -313,7 +313,7 @@ class AndroidExtEnv(ExtEnv): iw, ih = ih, iw # 下载权重文件 file_url = 'https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth' # 加载远程model - target_folder = '/Users/kit/Desktop/深度赋值/amzingproject/MetaGPT/workspace/weights' + target_folder = 'workspace/weights' file_path = download_model(file_url, target_folder) groundingdino_model = load_model(file_path, device=device).eval() in_coordinate, out_coordinate = det(image, "icon", groundingdino_model) # 检测icon @@ -324,7 +324,7 @@ class AndroidExtEnv(ExtEnv): return self.system_tap(tap_coordinate[0] * x, tap_coordinate[1] * y) else: - temp_file = "/Users/kit/Desktop/深度赋值/amzingproject/MetaGPT/workspace/temp" + temp_file = "workspace/temp" if not os.path.exists(temp_file): os.mkdir(temp_file) hash_table, clip_filter= [],[] From d0e898dcfada0c3b64286cd9661efb2d88e8355b Mon Sep 17 00:00:00 2001 From: kithib <1010465183@qq.com> Date: Fri, 19 Apr 2024 17:56:36 +0800 Subject: [PATCH 05/15] Merge remote-tracking branch 'origin/main' --- .../environment/android/android_ext_env.py | 42 +++++----- ..._SwinT_OGC.py => grounding_dino_config.py} | 0 .../android/text_icon_localization.py | 76 ++++++++----------- metagpt/utils/common.py | 20 ++++- metagpt/utils/download_modelweight.py | 22 ------ requirements.txt | 19 ----- setup.py | 23 +++++- 7 files changed, 91 insertions(+), 111 deletions(-) rename metagpt/environment/android/{GroundingDINO_SwinT_OGC.py => grounding_dino_config.py} (100%) delete mode 100644 metagpt/utils/download_modelweight.py diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index eb8a69330..cba0636c7 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # @Desc : The Android external environment to integrate with Android apps -import os import subprocess import clip import time @@ -25,7 +24,8 @@ from metagpt.environment.android.env_space import ( ) from metagpt.environment.base_env import ExtEnv, mark_as_readable, mark_as_writeable from metagpt.logs import logger -from metagpt.utils.download_modelweight import download_model +from metagpt.utils.common import download_model +from metagpt.const import DEFAULT_WORKSPACE_ROOT class AndroidExtEnv(ExtEnv): @@ -46,14 +46,14 @@ class AndroidExtEnv(ExtEnv): self.width = data.get("width", width) self.height = data.get("height", height) - self.create_device_path(self.screenshot_dir) - self.create_device_path(self.xml_dir) + #self.create_device_path(self.screenshot_dir) + #self.create_device_path(self.xml_dir) def reset( - self, - *, - seed: Optional[int] = None, - options: Optional[dict[str, Any]] = None, + self, + *, + seed: Optional[int] = None, + options: Optional[dict[str, Any]] = None, ) -> tuple[dict[str, Any], dict[str, Any]]: super().reset(seed=seed, options=options) @@ -247,10 +247,9 @@ class AndroidExtEnv(ExtEnv): exit_res = self.execute_adb_with_cmd(adb_cmd) return exit_res - @mark_as_writeable def _ocr_text(self, text: str) -> list: - if not os.path.exists(self.screenshot_dir): - os.makedirs(self.screenshot_dir) + if not self.screenshot_dir.exists(): + self.screenshot_dir.mkdir(parents=True, exist_ok=True) image = self.get_screenshot("screenshot", self.screenshot_dir) ocr_detection = pipeline(Tasks.ocr_detection, model="damo/cv_resnet18_ocr-detection-line-level_damo") ocr_recognition = pipeline(Tasks.ocr_recognition, model="damo/cv_convnextTiny_ocr-recognition-document_damo") @@ -302,8 +301,8 @@ class AndroidExtEnv(ExtEnv): @mark_as_writeable def user_click_icon(self, icon_shape_color: str) -> str: - if not os.path.exists(self.screenshot_dir): - os.makedirs(self.screenshot_dir) + if not self.screenshot_dir.exists(): + self.screenshot_dir.mkdir(parents=True, exist_ok=True) screenshot_path = self.get_screenshot("screenshot", self.screenshot_dir) image, device = screenshot_path, 'cpu' iw, ih = Image.open(image).size @@ -311,9 +310,8 @@ class AndroidExtEnv(ExtEnv): if iw > ih: x, y = y, x iw, ih = ih, iw - # 下载权重文件 file_url = 'https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth' # 加载远程model - target_folder = 'workspace/weights' + target_folder = Path(f'{DEFAULT_WORKSPACE_ROOT}/weights') file_path = download_model(file_url, target_folder) groundingdino_model = load_model(file_path, device=device).eval() in_coordinate, out_coordinate = det(image, "icon", groundingdino_model) # 检测icon @@ -324,22 +322,18 @@ class AndroidExtEnv(ExtEnv): return self.system_tap(tap_coordinate[0] * x, tap_coordinate[1] * y) else: - temp_file = "workspace/temp" - if not os.path.exists(temp_file): - os.mkdir(temp_file) - hash_table, clip_filter= [],[] + temp_file = Path(f"{DEFAULT_WORKSPACE_ROOT}/temp") + if not temp_file.exists(): + temp_file.mkdir(parents=True, exist_ok=True) + hash_table, clip_filter = [], [] for i, (td, box) in enumerate(zip(in_coordinate, out_coordinate)): if crop_for_clip(image, td, i, temp_file): hash_table.append(td) crop_image = f"{i}.jpg" - clip_filter.append(os.path.join(temp_file, crop_image)) + clip_filter.append(temp_file.joinpath(crop_image)) clip_model, clip_preprocess = clip.load("ViT-B/32", device=device) clip_filter = clip_for_icon(clip_model, clip_preprocess, clip_filter, icon_shape_color) final_box = hash_table[clip_filter] tap_coordinate = [(final_box[0] + final_box[2]) / 2, (final_box[1] + final_box[3]) / 2] tap_coordinate = [round(tap_coordinate[0] / iw, 2), round(tap_coordinate[1] / ih, 2)] return self.system_tap(tap_coordinate[0] * x, tap_coordinate[1] * y) - - - - diff --git a/metagpt/environment/android/GroundingDINO_SwinT_OGC.py b/metagpt/environment/android/grounding_dino_config.py similarity index 100% rename from metagpt/environment/android/GroundingDINO_SwinT_OGC.py rename to metagpt/environment/android/grounding_dino_config.py diff --git a/metagpt/environment/android/text_icon_localization.py b/metagpt/environment/android/text_icon_localization.py index 8c3d22c7c..4dd17ca60 100644 --- a/metagpt/environment/android/text_icon_localization.py +++ b/metagpt/environment/android/text_icon_localization.py @@ -1,3 +1,6 @@ +# The code in this file was modified by MobileAgent +# https://github.com/X-PLUG/MobileAgent.git + import math import clip import cv2 @@ -15,7 +18,7 @@ from PIL import Image, ImageDraw ################################## text_localization using ocr ####################### -def crop_image(img, position): +def crop_image(img: any, position: any) -> any: def distance(x1, y1, x2, y2): return math.sqrt(pow(x1 - x2, 2) + pow(y1 - y2, 2)) @@ -61,12 +64,12 @@ def crop_image(img, position): return dst -def calculate_size(box): +def calculate_size(box: any) -> any: return (box[2] - box[0]) * (box[3] - box[1]) -def order_point(coor): - arr = np.array(coor).reshape([4, 2]) +def order_point(cooperation: any) -> any: + arr = np.array(cooperation).reshape([4, 2]) sum_ = np.sum(arr, 0) centroid = sum_ / arr.shape[0] theta = np.arctan2(arr[:, 1] - centroid[1], arr[:, 0] - centroid[0]) @@ -78,11 +81,10 @@ def order_point(coor): return sort_points -def longest_common_substring_length(str1, str2): +def longest_common_substring_length(str1: str, str2: str) -> int: m = len(str1) n = len(str2) dp = [[0] * (n + 1) for _ in range(m + 1)] - for i in range(1, m + 1): for j in range(1, n + 1): if str1[i - 1] == str2[j - 1]: @@ -93,7 +95,7 @@ def longest_common_substring_length(str1, str2): return dp[m][n] -def ocr(image_path, prompt, ocr_detection, ocr_recognition, x, y): +def ocr(image_path: Path, prompt: str, ocr_detection: any, ocr_recognition: any, x: int, y: int) -> any: text_data = [] coordinate = [] image = Image.open(image_path) @@ -191,54 +193,41 @@ def ocr(image_path, prompt, ocr_detection, ocr_recognition, x, y): ################################## icon_localization using clip ####################### -def calculate_iou(box1, box2): - xA = max(box1[0], box2[0]) - yA = max(box1[1], box2[1]) - xB = min(box1[2], box2[2]) - yB = min(box1[3], box2[3]) +def calculate_iou(box1: list, box2: list) -> float: + x_a = max(box1[0], box2[0]) + y_a = max(box1[1], box2[1]) + x_b = min(box1[2], box2[2]) + y_b = min(box1[3], box2[3]) - interArea = max(0, xB - xA) * max(0, yB - yA) - box1Area = (box1[2] - box1[0]) * (box1[3] - box1[1]) - box2Area = (box2[2] - box2[0]) * (box2[3] - box2[1]) - unionArea = box1Area + box2Area - interArea - iou = interArea / unionArea + inter_area = max(0, x_b - x_a) * max(0, y_b - y_a) + box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1]) + box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1]) + union_area = box1_area + box2_area - inter_area + iou = inter_area / union_area return iou -def crop(image, box, i, text_data=None): - image = Image.open(image) - if text_data: - draw = ImageDraw.Draw(image) - draw.rectangle(((text_data[0], text_data[1]), (text_data[2], text_data[3])), outline="red", width=5) - # font_size = int((text_data[3] - text_data[1])*0.75) - # font = ImageFont.truetype("arial.ttf", font_size) - # draw.text((text_data[0]+5, text_data[1]+5), str(i), font=font, fill="red") - - cropped_image = image.crop(box) - cropped_image.save(f"./temp/{i}.jpg") - - -def in_box(box, target): +def in_box(box: list, target: list) -> bool: if (box[0] > target[0]) and (box[1] > target[1]) and (box[2] < target[2]) and (box[3] < target[3]): return True else: return False -def crop_for_clip(image, box, i, temp_file): +def crop_for_clip(image: any, box: any, i: int, temp_file: Path) -> bool: image = Image.open(image) w, h = image.size bound = [0, 0, w, h] if in_box(box, bound): cropped_image = image.crop(box) - cropped_image.save(f"{temp_file}/{i}.jpg") + cropped_image.save(temp_file.joinpath(f"{i}.jpg")) return True else: return False -def clip_for_icon(clip_model, clip_preprocess, images, prompt): +def clip_for_icon(clip_model: any, clip_preprocess: any, images: any, prompt: str) -> any: image_features = [] for image_file in images: image = clip_preprocess(Image.open(image_file)).unsqueeze(0).to(next(clip_model.parameters()).device) @@ -258,7 +247,7 @@ def clip_for_icon(clip_model, clip_preprocess, images, prompt): return pos -def transform_image(image_pil): +def transform_image(image_pil: any) -> any: transform = T.Compose( [ T.RandomResize([800], max_size=1333), @@ -270,8 +259,8 @@ def transform_image(image_pil): return image -def load_model(model_checkpoint_path, device): - model_config_path = 'GroundingDINO_SwinT_OGC.py' +def load_model(model_checkpoint_path: Path, device: str) -> any: + model_config_path = 'grounding_dino_config.py' args = SLConfig.fromfile(model_config_path) args.device = device model = build_model(args) @@ -282,7 +271,7 @@ def load_model(model_checkpoint_path, device): return model -def get_grounding_output(model, image, caption, box_threshold, text_threshold, with_logits=True): +def get_grounding_output(model: any, image: any, caption: str, box_threshold: any, text_threshold: any, with_logits=True) -> any: caption = caption.lower() caption = caption.strip() if not caption.endswith("."): @@ -317,7 +306,7 @@ def get_grounding_output(model, image, caption, box_threshold, text_threshold, w return boxes_filt, torch.Tensor(scores), pred_phrases -def remove_boxes(boxes_filt, size, iou_threshold=0.5): +def remove_boxes(boxes_filt: any, size: any, iou_threshold=0.5) -> any: boxes_to_remove = set() for i in range(len(boxes_filt)): @@ -339,7 +328,7 @@ def remove_boxes(boxes_filt, size, iou_threshold=0.5): return boxes_filt -def det(input_image, text_prompt, groundingdino_model, box_threshold=0.05, text_threshold=0.5): +def det(input_image: any, text_prompt: str, groundingdino_model: any, box_threshold=0.05, text_threshold=0.5) -> any: image = Image.open(input_image) size = image.size @@ -372,7 +361,7 @@ def det(input_image, text_prompt, groundingdino_model, box_threshold=0.05, text_ return image_data, coordinate -def get_screenshot_only(screenshot_dir: Path) -> str: +def get_screenshot_only(screenshot_dir: Path) -> Path: command = " adb shell rm /sdcard/screenshot.png" subprocess.run(command, capture_output=True, text=True, shell=True) time.sleep(0.1) @@ -381,8 +370,8 @@ def get_screenshot_only(screenshot_dir: Path) -> str: time.sleep(0.1) command = f"adb pull /sdcard/screenshot.png {screenshot_dir}" subprocess.run(command, capture_output=True, text=True, shell=True) - image_path = f"{screenshot_dir}/screenshot.png" - save_path = f"{screenshot_dir}/screenshot.jpg" + image_path = Path(f"{screenshot_dir}/screenshot.png") + save_path = Path(f"{screenshot_dir}/screenshot.jpg") image = Image.open(image_path) original_width, original_height = image.size new_width = int(original_width * 0.5) @@ -391,4 +380,3 @@ def get_screenshot_only(screenshot_dir: Path) -> str: resized_image.convert("RGB").save(save_path, "JPEG") time.sleep(0.1) return save_path - diff --git a/metagpt/utils/common.py b/metagpt/utils/common.py index 0876b85ad..982e6921b 100644 --- a/metagpt/utils/common.py +++ b/metagpt/utils/common.py @@ -219,7 +219,7 @@ class OutputParser: if start_index != -1 and end_index != -1: # Extract the structure part - structure_text = text[start_index : end_index + 1] + structure_text = text[start_index: end_index + 1] try: # Attempt to convert the text to a Python data type using ast.literal_eval @@ -841,3 +841,21 @@ def get_markdown_codeblock_type(filename: str) -> str: "application/sql": "sql", } return mappings.get(mime_type, "text") + + +def download_model(file_url: str, target_folder: Path) -> Path: + file_name = file_url.split('/')[-1] + file_path = target_folder.joinpath(f"{file_name}") + if not file_path.exists(): + file_path.mkdir(parents=True, exist_ok=True) + try: + response = requests.get(file_url, stream=True) + response.raise_for_status() # 检查请求是否成功 + # 保存文件 + with open(file_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + logger.info(f'权重文件已下载并保存至 {file_path}') + except requests.exceptions.HTTPError as err: + logger.info(f'权重文件下载过程中发生错误: {err}') + return file_path diff --git a/metagpt/utils/download_modelweight.py b/metagpt/utils/download_modelweight.py deleted file mode 100644 index 2b8bcf41b..000000000 --- a/metagpt/utils/download_modelweight.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -import requests -from pathlib import Path - - -def download_model(file_url: str, target_folder: str) -> str: - file_name = file_url.split('/')[-1] # 文件名(从URL中提取) - file_path = os.path.join(target_folder, file_name) # 完整的文件路径 - if not os.path.exists(target_folder): - os.makedirs(target_folder) - # 发起GET请求下载文件 - try: - response = requests.get(file_url, stream=True) - response.raise_for_status() # 检查请求是否成功 - # 保存文件 - with open(file_path, 'wb') as f: - for chunk in response.iter_content(chunk_size=8192): - f.write(chunk) - print(f'权重文件已下载并保存至 {file_path}') - except requests.exceptions.HTTPError as err: - print(f'权重文件下载过程中发生错误: {err}') - return file_path diff --git a/requirements.txt b/requirements.txt index 46832e943..c6d46fa25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,23 +71,4 @@ dashscope==1.14.1 rank-bm25==0.2.2 # for tool recommendation jieba==0.42.1 # for tool recommendation gymnasium==0.29.1 - -# for clip and ocr -git+https://github.com/openai/CLIP.git -protobuf<3.20,>=3.9.2 -modelscope -tensorflow==2.9.1; os_name == 'linux' -tensorflow-macos==2.9; os_name == 'darwin' -keras==2.9.0 -torch -torchvision -transformers -opencv-python -matplotlib -pycocotools timm -SentencePiece -tf_slim -tf_keras -pyclipper -shapely \ No newline at end of file diff --git a/setup.py b/setup.py index e43bf3ed0..d33ac8e0f 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,28 @@ extras_require = { "llama-index-vector-stores-chroma==0.1.6", "docx2txt==0.8", ], - "android_assistant": ["pyshine==0.0.9", "opencv-python==4.6.0.66"], + "android_assistant": [ + "pyshine==0.0.9", + "opencv-python==4.6.0.66", + "git+https://github.com/openai/CLIP.git", + "protobuf<3.20,>=3.9.2", + "modelscope", + "tensorflow==2.9.1; os_name == 'linux'", + "tensorflow-macos==2.9; os_name == 'darwin'", + "keras==2.9.0", + "torch", + "torchvision", + "transformers", + "opencv-python", + "matplotlib", + "pycocotools", + "SentencePiece", + "tf_slim", + "tf_keras", + "pyclipper", + "shapely", + "groundingdino-py", + ], } extras_require["test"] = [ From 1b1c88149087d89fc09aaebb25ab19329fa0682f Mon Sep 17 00:00:00 2001 From: kithib <1010465183@qq.com> Date: Fri, 19 Apr 2024 18:17:58 +0800 Subject: [PATCH 06/15] Merge remote-tracking branch 'origin/main' --- requirements.txt | 1 + setup.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c6d46fa25..75d03af94 100644 --- a/requirements.txt +++ b/requirements.txt @@ -72,3 +72,4 @@ rank-bm25==0.2.2 # for tool recommendation jieba==0.42.1 # for tool recommendation gymnasium==0.29.1 timm + diff --git a/setup.py b/setup.py index d33ac8e0f..22782a5c3 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ extras_require = { "android_assistant": [ "pyshine==0.0.9", "opencv-python==4.6.0.66", - "git+https://github.com/openai/CLIP.git", "protobuf<3.20,>=3.9.2", "modelscope", "tensorflow==2.9.1; os_name == 'linux'", @@ -114,4 +113,7 @@ setup( ], }, include_package_data=True, + dependency_links=[ + 'git+https://github.com/openai/CLIP.git', + ], ) From 5e882563a37069588b061caac2d79df8ee89dc9f Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Wed, 24 Apr 2024 21:44:07 +0800 Subject: [PATCH 07/15] Merge remote-tracking branch 'origin/main' --- metagpt/environment/android/android_ext_env.py | 8 ++++---- setup.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index cba0636c7..9a3e5a4c0 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -45,9 +45,8 @@ class AndroidExtEnv(ExtEnv): (width, height) = self.device_shape self.width = data.get("width", width) self.height = data.get("height", height) - - #self.create_device_path(self.screenshot_dir) - #self.create_device_path(self.xml_dir) + self.create_device_path(self.screenshot_dir) + self.create_device_path(self.xml_dir) def reset( self, @@ -269,7 +268,7 @@ class AndroidExtEnv(ExtEnv): ocr_result[0], ocr_result[1], ocr_result[2], ocr_result[3], ocr_result[4], ocr_result[5]) if len(in_coordinate) == 0: logger.info(f"No App named {app_name}.") - return "no" + return "no app here" else: tap_coordinate = [ (in_coordinate[0][0] + in_coordinate[0][2]) / 2, @@ -336,4 +335,5 @@ class AndroidExtEnv(ExtEnv): final_box = hash_table[clip_filter] tap_coordinate = [(final_box[0] + final_box[2]) / 2, (final_box[1] + final_box[3]) / 2] tap_coordinate = [round(tap_coordinate[0] / iw, 2), round(tap_coordinate[1] / ih, 2)] + print(tap_coordinate[0] * x, tap_coordinate[1] * y) return self.system_tap(tap_coordinate[0] * x, tap_coordinate[1] * y) diff --git a/setup.py b/setup.py index 22782a5c3..1368a67fd 100644 --- a/setup.py +++ b/setup.py @@ -62,6 +62,7 @@ extras_require = { "pyclipper", "shapely", "groundingdino-py", + "datasets==2.18.0", ], } From 3da74ec00de98eda5cab64f82f7ceb61e538062e Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Wed, 24 Apr 2024 21:55:53 +0800 Subject: [PATCH 08/15] Merge remote-tracking branch 'origin/main' --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 75d03af94..93816c8ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,5 +71,5 @@ dashscope==1.14.1 rank-bm25==0.2.2 # for tool recommendation jieba==0.42.1 # for tool recommendation gymnasium==0.29.1 -timm + From 84a8c0d0bd7fc61d76fd573e151010df25cf06b1 Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Fri, 26 Apr 2024 11:52:54 +0800 Subject: [PATCH 09/15] Merge remote-tracking branch 'origin/main' --- .../environment/android/android_ext_env.py | 36 +++++++++++++++---- .../android/text_icon_localization.py | 27 +++----------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index 9a3e5a4c0..e15d7fe3f 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -34,10 +34,16 @@ class AndroidExtEnv(ExtEnv): xml_dir: Optional[Path] = Field(default=None) width: int = Field(default=720, description="device screen width") height: int = Field(default=1080, description="device screen height") + cv_model_status: dict = Field(default=None, description="Record model loading status") def __init__(self, **data: Any): super().__init__(**data) device_id = data.get("device_id") + self.cv_model_status = { + 'ocr_detection_loaded': False, + 'ocr_recognition_loaded': False, + 'clip_model_loaded': False + } if device_id: devices = self.list_devices() if device_id not in devices: @@ -45,8 +51,8 @@ class AndroidExtEnv(ExtEnv): (width, height) = self.device_shape self.width = data.get("width", width) self.height = data.get("height", height) - self.create_device_path(self.screenshot_dir) - self.create_device_path(self.xml_dir) + #self.create_device_path(self.screenshot_dir) + #self.create_device_path(self.xml_dir) def reset( self, @@ -167,7 +173,16 @@ class AndroidExtEnv(ExtEnv): if pull_res != ADB_EXEC_FAIL: res = ss_local_path else: - res = get_screenshot_only(local_save_dir) + ss_cmd = f"{self.adb_prefix_shell} rm /sdcard/screenshot.png" + ss_res = self.execute_adb_with_cmd(ss_cmd) + time.sleep(0.1) + ss_cmd = f"{self.adb_prefix_shell} screencap -p /sdcard/screenshot.png" + ss_res = self.execute_adb_with_cmd(ss_cmd) + time.sleep(0.1) + ss_cmd = f"{self.adb_prefix} pull /sdcard/screenshot.png {self.screenshot_dir}" + ss_res = self.execute_adb_with_cmd(ss_cmd) + image_path = Path(f"{self.screenshot_dir}/screenshot.png") + res = image_path return Path(res) @mark_as_readable @@ -246,12 +261,17 @@ class AndroidExtEnv(ExtEnv): exit_res = self.execute_adb_with_cmd(adb_cmd) return exit_res + def _ocr_text(self, text: str) -> list: if not self.screenshot_dir.exists(): self.screenshot_dir.mkdir(parents=True, exist_ok=True) image = self.get_screenshot("screenshot", self.screenshot_dir) - ocr_detection = pipeline(Tasks.ocr_detection, model="damo/cv_resnet18_ocr-detection-line-level_damo") - ocr_recognition = pipeline(Tasks.ocr_recognition, model="damo/cv_convnextTiny_ocr-recognition-document_damo") + if self.cv_model_status['ocr_detection_loaded'] == False: + ocr_detection = pipeline(Tasks.ocr_detection, model="damo/cv_resnet18_ocr-detection-line-level_damo") + self.cv_model_status['ocr_detection_loaded'] = True + if self.cv_model_status['ocr_recognition_loaded'] == False: + ocr_recognition = pipeline(Tasks.ocr_recognition, model="damo/cv_convnextTiny_ocr-recognition-document_damo") + self.cv_model_status['ocr_recognition_loaded'] == True iw, ih = Image.open(image).size x, y = self.device_shape if iw > ih: @@ -312,7 +332,9 @@ class AndroidExtEnv(ExtEnv): file_url = 'https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth' # 加载远程model target_folder = Path(f'{DEFAULT_WORKSPACE_ROOT}/weights') file_path = download_model(file_url, target_folder) - groundingdino_model = load_model(file_path, device=device).eval() + if self.cv_model_status['clip_model_loaded'] == False: + groundingdino_model = load_model(file_path, device=device).eval() + self.cv_model_status['clip_model_loaded'] = True in_coordinate, out_coordinate = det(image, "icon", groundingdino_model) # 检测icon if len(out_coordinate) == 1: # only one icon tap_coordinate = [(in_coordinate[0][0] + in_coordinate[0][2]) / 2, @@ -328,7 +350,7 @@ class AndroidExtEnv(ExtEnv): for i, (td, box) in enumerate(zip(in_coordinate, out_coordinate)): if crop_for_clip(image, td, i, temp_file): hash_table.append(td) - crop_image = f"{i}.jpg" + crop_image = f"{i}.png" clip_filter.append(temp_file.joinpath(crop_image)) clip_model, clip_preprocess = clip.load("ViT-B/32", device=device) clip_filter = clip_for_icon(clip_model, clip_preprocess, clip_filter, icon_shape_color) diff --git a/metagpt/environment/android/text_icon_localization.py b/metagpt/environment/android/text_icon_localization.py index 4dd17ca60..2021acec4 100644 --- a/metagpt/environment/android/text_icon_localization.py +++ b/metagpt/environment/android/text_icon_localization.py @@ -221,7 +221,7 @@ def crop_for_clip(image: any, box: any, i: int, temp_file: Path) -> bool: bound = [0, 0, w, h] if in_box(box, bound): cropped_image = image.crop(box) - cropped_image.save(temp_file.joinpath(f"{i}.jpg")) + cropped_image.save(temp_file.joinpath(f"{i}.png")) return True else: return False @@ -271,7 +271,7 @@ def load_model(model_checkpoint_path: Path, device: str) -> any: return model -def get_grounding_output(model: any, image: any, caption: str, box_threshold: any, text_threshold: any, with_logits=True) -> any: +def get_grounding_output(model: any, image: any, caption: str, box_threshold: any, text_threshold: any, with_logits: bool = True) -> any: caption = caption.lower() caption = caption.strip() if not caption.endswith("."): @@ -306,7 +306,7 @@ def get_grounding_output(model: any, image: any, caption: str, box_threshold: an return boxes_filt, torch.Tensor(scores), pred_phrases -def remove_boxes(boxes_filt: any, size: any, iou_threshold=0.5) -> any: +def remove_boxes(boxes_filt: any, size: any, iou_threshold: float = 0.5) -> any: boxes_to_remove = set() for i in range(len(boxes_filt)): @@ -328,7 +328,7 @@ def remove_boxes(boxes_filt: any, size: any, iou_threshold=0.5) -> any: return boxes_filt -def det(input_image: any, text_prompt: str, groundingdino_model: any, box_threshold=0.05, text_threshold=0.5) -> any: +def det(input_image: any, text_prompt: str, groundingdino_model: any, box_threshold:float = 0.05, text_threshold:float = 0.5) -> any: image = Image.open(input_image) size = image.size @@ -361,22 +361,3 @@ def det(input_image: any, text_prompt: str, groundingdino_model: any, box_thresh return image_data, coordinate -def get_screenshot_only(screenshot_dir: Path) -> Path: - command = " adb shell rm /sdcard/screenshot.png" - subprocess.run(command, capture_output=True, text=True, shell=True) - time.sleep(0.1) - command = "adb shell screencap -p /sdcard/screenshot.png" - subprocess.run(command, capture_output=True, text=True, shell=True) - time.sleep(0.1) - command = f"adb pull /sdcard/screenshot.png {screenshot_dir}" - subprocess.run(command, capture_output=True, text=True, shell=True) - image_path = Path(f"{screenshot_dir}/screenshot.png") - save_path = Path(f"{screenshot_dir}/screenshot.jpg") - image = Image.open(image_path) - original_width, original_height = image.size - new_width = int(original_width * 0.5) - new_height = int(original_height * 0.5) - resized_image = image.resize((new_width, new_height)) - resized_image.convert("RGB").save(save_path, "JPEG") - time.sleep(0.1) - return save_path From 72b3880ca8ec2d03c999409c6fbab6cf04c5648d Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Fri, 26 Apr 2024 11:54:34 +0800 Subject: [PATCH 10/15] Merge remote-tracking branch 'origin/main' --- metagpt/environment/android/android_ext_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index e15d7fe3f..8b9638fb7 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -51,8 +51,8 @@ class AndroidExtEnv(ExtEnv): (width, height) = self.device_shape self.width = data.get("width", width) self.height = data.get("height", height) - #self.create_device_path(self.screenshot_dir) - #self.create_device_path(self.xml_dir) + self.create_device_path(self.screenshot_dir) + self.create_device_path(self.xml_dir) def reset( self, From 17580333b85a2d4ab6540883394751fee1a3273b Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Fri, 26 Apr 2024 11:56:18 +0800 Subject: [PATCH 11/15] Merge remote-tracking branch 'origin/main' --- metagpt/environment/android/android_ext_env.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index 8b9638fb7..bf64c0988 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -329,10 +329,10 @@ class AndroidExtEnv(ExtEnv): if iw > ih: x, y = y, x iw, ih = ih, iw - file_url = 'https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth' # 加载远程model - target_folder = Path(f'{DEFAULT_WORKSPACE_ROOT}/weights') - file_path = download_model(file_url, target_folder) if self.cv_model_status['clip_model_loaded'] == False: + file_url = 'https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth' # 加载远程model + target_folder = Path(f'{DEFAULT_WORKSPACE_ROOT}/weights') + file_path = download_model(file_url, target_folder) groundingdino_model = load_model(file_path, device=device).eval() self.cv_model_status['clip_model_loaded'] = True in_coordinate, out_coordinate = det(image, "icon", groundingdino_model) # 检测icon From cf9d86b83263e699f31b0b383ecc80f8a0cc1988 Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Mon, 29 Apr 2024 15:19:42 +0800 Subject: [PATCH 12/15] Merge remote-tracking branch 'origin/main' --- .../environment/android/android_ext_env.py | 57 +++++++++---------- .../android/text_icon_localization.py | 2 +- requirements.txt | 4 +- setup.py | 1 + 4 files changed, 29 insertions(+), 35 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index bf64c0988..060d956a3 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -28,22 +28,31 @@ from metagpt.utils.common import download_model from metagpt.const import DEFAULT_WORKSPACE_ROOT +def load_cv_model(device: str = "cpu") -> any: + ocr_detection = pipeline(Tasks.ocr_detection, model="damo/cv_resnet18_ocr-detection-line-level_damo") + ocr_recognition = pipeline(Tasks.ocr_recognition, + model="damo/cv_convnextTiny_ocr-recognition-document_damo") + file_url = "https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth" + target_folder = Path(f"{DEFAULT_WORKSPACE_ROOT}/weights") + file_path = download_model(file_url, target_folder) + groundingdino_model = load_model(file_path, device=device).eval() + return ocr_detection, ocr_recognition, groundingdino_model + + class AndroidExtEnv(ExtEnv): device_id: Optional[str] = Field(default=None) screenshot_dir: Optional[Path] = Field(default=None) xml_dir: Optional[Path] = Field(default=None) width: int = Field(default=720, description="device screen width") height: int = Field(default=1080, description="device screen height") - cv_model_status: dict = Field(default=None, description="Record model loading status") + ocr_detection: any = Field(default=None, description="ocr detection model") + ocr_recognition: any = Field(default=None, description="ocr recognition model") + groundingdino_model: any = Field(default=None, description="clip groundingdino model") def __init__(self, **data: Any): super().__init__(**data) device_id = data.get("device_id") - self.cv_model_status = { - 'ocr_detection_loaded': False, - 'ocr_recognition_loaded': False, - 'clip_model_loaded': False - } + self.ocr_detection, self.ocr_recognition, self.groundingdino_model = load_cv_model() if device_id: devices = self.list_devices() if device_id not in devices: @@ -51,8 +60,8 @@ class AndroidExtEnv(ExtEnv): (width, height) = self.device_shape self.width = data.get("width", width) self.height = data.get("height", height) - self.create_device_path(self.screenshot_dir) - self.create_device_path(self.xml_dir) + #self.create_device_path(self.screenshot_dir) + #self.create_device_path(self.xml_dir) def reset( self, @@ -173,16 +182,16 @@ class AndroidExtEnv(ExtEnv): if pull_res != ADB_EXEC_FAIL: res = ss_local_path else: - ss_cmd = f"{self.adb_prefix_shell} rm /sdcard/screenshot.png" + ss_cmd = f"{self.adb_prefix_shell} rm /sdcard/{ss_name}.png" ss_res = self.execute_adb_with_cmd(ss_cmd) time.sleep(0.1) - ss_cmd = f"{self.adb_prefix_shell} screencap -p /sdcard/screenshot.png" + ss_cmd = f"{self.adb_prefix_shell} screencap -p /sdcard/{ss_name}.png" ss_res = self.execute_adb_with_cmd(ss_cmd) time.sleep(0.1) - ss_cmd = f"{self.adb_prefix} pull /sdcard/screenshot.png {self.screenshot_dir}" + ss_cmd = f"{self.adb_prefix} pull /sdcard/{ss_name}.png {self.screenshot_dir}" ss_res = self.execute_adb_with_cmd(ss_cmd) - image_path = Path(f"{self.screenshot_dir}/screenshot.png") - res = image_path + image_path = Path(f"{self.screenshot_dir}/{ss_name}.png") + res = image_path return Path(res) @mark_as_readable @@ -261,23 +270,16 @@ class AndroidExtEnv(ExtEnv): exit_res = self.execute_adb_with_cmd(adb_cmd) return exit_res - def _ocr_text(self, text: str) -> list: if not self.screenshot_dir.exists(): self.screenshot_dir.mkdir(parents=True, exist_ok=True) image = self.get_screenshot("screenshot", self.screenshot_dir) - if self.cv_model_status['ocr_detection_loaded'] == False: - ocr_detection = pipeline(Tasks.ocr_detection, model="damo/cv_resnet18_ocr-detection-line-level_damo") - self.cv_model_status['ocr_detection_loaded'] = True - if self.cv_model_status['ocr_recognition_loaded'] == False: - ocr_recognition = pipeline(Tasks.ocr_recognition, model="damo/cv_convnextTiny_ocr-recognition-document_damo") - self.cv_model_status['ocr_recognition_loaded'] == True iw, ih = Image.open(image).size x, y = self.device_shape if iw > ih: x, y = y, x iw, ih = ih, iw - in_coordinate, out_coordinate = ocr(image, text, ocr_detection, ocr_recognition, iw, ih) + in_coordinate, out_coordinate = ocr(image, text, self.ocr_detection, self.ocr_recognition, iw, ih) output_list = [in_coordinate, out_coordinate, x, y, iw, ih, image] return output_list @@ -323,19 +325,13 @@ class AndroidExtEnv(ExtEnv): if not self.screenshot_dir.exists(): self.screenshot_dir.mkdir(parents=True, exist_ok=True) screenshot_path = self.get_screenshot("screenshot", self.screenshot_dir) - image, device = screenshot_path, 'cpu' + image= screenshot_path iw, ih = Image.open(image).size x, y = self.device_shape if iw > ih: x, y = y, x iw, ih = ih, iw - if self.cv_model_status['clip_model_loaded'] == False: - file_url = 'https://huggingface.co/ShilongLiu/GroundingDINO/blob/main/groundingdino_swint_ogc.pth' # 加载远程model - target_folder = Path(f'{DEFAULT_WORKSPACE_ROOT}/weights') - file_path = download_model(file_url, target_folder) - groundingdino_model = load_model(file_path, device=device).eval() - self.cv_model_status['clip_model_loaded'] = True - in_coordinate, out_coordinate = det(image, "icon", groundingdino_model) # 检测icon + in_coordinate, out_coordinate = det(image, "icon", self.groundingdino_model) # 检测icon if len(out_coordinate) == 1: # only one icon tap_coordinate = [(in_coordinate[0][0] + in_coordinate[0][2]) / 2, (in_coordinate[0][1] + in_coordinate[0][3]) / 2] @@ -344,8 +340,7 @@ class AndroidExtEnv(ExtEnv): else: temp_file = Path(f"{DEFAULT_WORKSPACE_ROOT}/temp") - if not temp_file.exists(): - temp_file.mkdir(parents=True, exist_ok=True) + temp_file.mkdir(parents=True, exist_ok=True) hash_table, clip_filter = [], [] for i, (td, box) in enumerate(zip(in_coordinate, out_coordinate)): if crop_for_clip(image, td, i, temp_file): diff --git a/metagpt/environment/android/text_icon_localization.py b/metagpt/environment/android/text_icon_localization.py index 2021acec4..60d62ed03 100644 --- a/metagpt/environment/android/text_icon_localization.py +++ b/metagpt/environment/android/text_icon_localization.py @@ -260,7 +260,7 @@ def transform_image(image_pil: any) -> any: def load_model(model_checkpoint_path: Path, device: str) -> any: - model_config_path = 'grounding_dino_config.py' + model_config_path = "grounding_dino_config.py" args = SLConfig.fromfile(model_config_path) args.device = device model = build_model(args) diff --git a/requirements.txt b/requirements.txt index 93816c8ef..d150d61f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,6 +70,4 @@ qianfan==0.3.2 dashscope==1.14.1 rank-bm25==0.2.2 # for tool recommendation jieba==0.42.1 # for tool recommendation -gymnasium==0.29.1 - - +gymnasium==0.29.1 \ No newline at end of file diff --git a/setup.py b/setup.py index 1368a67fd..daa86f88c 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ extras_require = { "protobuf<3.20,>=3.9.2", "modelscope", "tensorflow==2.9.1; os_name == 'linux'", + "tensorflow==2.9.1; os_name == 'win32'", "tensorflow-macos==2.9; os_name == 'darwin'", "keras==2.9.0", "torch", From 19cc91a38aeda616ac3e168e6dbc9d2e6c33e4dd Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Mon, 29 Apr 2024 15:21:13 +0800 Subject: [PATCH 13/15] Merge remote-tracking branch 'origin/main' --- metagpt/environment/android/android_ext_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index 060d956a3..39c3158bc 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -60,8 +60,8 @@ class AndroidExtEnv(ExtEnv): (width, height) = self.device_shape self.width = data.get("width", width) self.height = data.get("height", height) - #self.create_device_path(self.screenshot_dir) - #self.create_device_path(self.xml_dir) + self.create_device_path(self.screenshot_dir) + self.create_device_path(self.xml_dir) def reset( self, From 9855d4ab1cc90def55c7239271cc7d38bf1558eb Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Tue, 7 May 2024 08:57:39 +0800 Subject: [PATCH 14/15] Merge remote-tracking branch 'origin/main' --- setup.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index daa86f88c..bd9885bc1 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ extras_require = { "shapely", "groundingdino-py", "datasets==2.18.0", + "clip-openai" ], } @@ -115,7 +116,5 @@ setup( ], }, include_package_data=True, - dependency_links=[ - 'git+https://github.com/openai/CLIP.git', - ], + ) From 2b08ad9d08af6a52ae9b2fb6b71360798cfc07cf Mon Sep 17 00:00:00 2001 From: kit <101046518@qq.com> Date: Fri, 17 May 2024 16:10:18 +0800 Subject: [PATCH 15/15] Merge remote-tracking branch 'origin/main' --- metagpt/environment/android/android_ext_env.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/metagpt/environment/android/android_ext_env.py b/metagpt/environment/android/android_ext_env.py index 39c3158bc..78f27923f 100644 --- a/metagpt/environment/android/android_ext_env.py +++ b/metagpt/environment/android/android_ext_env.py @@ -271,8 +271,6 @@ class AndroidExtEnv(ExtEnv): return exit_res def _ocr_text(self, text: str) -> list: - if not self.screenshot_dir.exists(): - self.screenshot_dir.mkdir(parents=True, exist_ok=True) image = self.get_screenshot("screenshot", self.screenshot_dir) iw, ih = Image.open(image).size x, y = self.device_shape @@ -322,8 +320,6 @@ class AndroidExtEnv(ExtEnv): @mark_as_writeable def user_click_icon(self, icon_shape_color: str) -> str: - if not self.screenshot_dir.exists(): - self.screenshot_dir.mkdir(parents=True, exist_ok=True) screenshot_path = self.get_screenshot("screenshot", self.screenshot_dir) image= screenshot_path iw, ih = Image.open(image).size