# This file is created by Minyi Liu (GitHub ID: MiniMinyi) # The hash algorithm is copied from: # https://github.com/hjaurum/DHash/blob/master/dHash.py def load_image_from_path(img_path): """ Load an image from path :param img_path: The path to the image :return: """ import cv2 import numpy as np # Fix for reading images with Chinese paths on Windows # Use IMREAD_COLOR to ensure 3 channels (BGR) as cv2.imread does by default return cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), cv2.IMREAD_COLOR) def load_image_from_buf(img_bytes): """ Load an image from a byte array :param img_bytes: The byte array of an image :return: """ import cv2 import numpy img_bytes = numpy.frombuffer(img_bytes, dtype=numpy.uint8) return cv2.imdecode(img_bytes, cv2.IMREAD_UNCHANGED) # ------------------------------------------------------------- # 辅助函数:将 OpenCV 图像对象编码为 Base64 # ------------------------------------------------------------- import cv2 import numpy as np import base64 import os from typing import Optional, List, Tuple, Dict, Any # 全局变量用于缓存模型实例 _model_handler = None _model_config = None class OmniParserModelManager: """ OmniParser模型管理器,使用单例模式确保模型只初始化一次 """ _instance = None _handler = None def __new__(cls): if cls._instance is None: cls._instance = super(OmniParserModelManager, cls).__new__(cls) return cls._instance def __init__(self): if self._handler is None: self._initialize_model() def _initialize_model(self): """初始化OmniParser模型""" from .handler import EndpointHandler # 默认配置 config = { "model_dir": os.path.join(os.path.dirname(__file__), "."), "mode": "custom", "ocr_languages": ["ch_sim", "en"], "image_size": 1280, "bbox_threshold": 0.2, "iou_threshold": 0.5, } print("初始化OmniParser模型...") self._handler = EndpointHandler( model_dir=config["model_dir"], enable_yolo=True, enable_ocr=True, enable_caption=False, ocr_languages=config["ocr_languages"], ) self._config = config print("OmniParser模型初始化完成") def get_handler(self): """获取模型处理器实例""" return self._handler def get_config(self): """获取模型配置""" return self._config def get_model_manager() -> OmniParserModelManager: """获取模型管理器单例实例""" return OmniParserModelManager() def load_image_from_buf(img_bytes): """ Load image from bytes :param img_bytes: bytes of image :return: numpy.ndarray, representing an image in opencv """ import cv2 import numpy img_bytes = numpy.frombuffer(img_bytes, dtype=numpy.uint8) return cv2.imdecode(img_bytes, cv2.IMREAD_UNCHANGED) def _encode_cv_image(img): """ 将 OpenCV 图像 (numpy.ndarray) 编码为 Base64 字符串。 """ import cv2 import base64 # 将图像编码为 PNG 格式的字节流 success, encoded_image = cv2.imencode(".png", img) if not success: raise ValueError("无法将图像编码为 Base64") # 转换为 Base64 字符串 (不带 MIME 头) raw_b64 = base64.b64encode(encoded_image).decode("ascii") # 加上 MIME 头 return f"data:image/png;base64,{raw_b64}" def enhance_contrast_gamma(image, gamma=1.0): """ Apply Power Law Transformation to the image. Output = (Input/255) ^ gamma * 255 Gamma > 1.0 will darken the image (crush shadows). Gamma < 1.0 will brighten the image. """ if gamma == 1.0: return image import numpy as np import cv2 # Direct power law transformation table = np.array([((i / 255.0) ** gamma) * 255 for i in np.arange(0, 256)]).astype("uint8") return cv2.LUT(image, table) def find_views(img) -> List[Dict[str, Any]]: """ 使用 OmniParser 模型查找给定 UI 截图中的矩形视图。 默认应用 Gamma=4.0 的对比度增强以压暗背景(处理弹窗场景)。 :param img: numpy.ndarray, representing an image in opencv :return: List[ViewDict] - 符合统一 ViewDict 格式的视图列表 """ import cv2 # 默认应用 Gamma 校正 (Gamma=4.0) 以压暗背景 # 用户需求:默认执行此预处理,无需外部传参 print(f"[CV] Applying Default Gamma Correction (gamma=4.0)...") img = enhance_contrast_gamma(img, gamma=4.0) # 保存处理后的图像到当前目录,方便查看 import os import cv2 # output_path = os.path.join(".", "gamma_enhanced.png") # cv2.imwrite(output_path, img) # print(f"[CV] 已保存 Gamma 校正后的图像到: {os.path.abspath(output_path)}") # 获取图像的原始尺寸 height, width = img.shape[:2] # 获取模型管理器实例(单例模式,确保只初始化一次) model_manager = get_model_manager() handler = model_manager.get_handler() config = model_manager.get_config() # 构造推理请求 image_b64 = _encode_cv_image(img) payload = { "inputs": { "image": image_b64, # 模型推理尺寸可以设为一个标准值,例如 1280x1280 "image_size": {"w": config["image_size"], "h": config["image_size"]}, "bbox_threshold": config["bbox_threshold"], "iou_threshold": config["iou_threshold"], } } # 执行推理 result = handler(payload) # 解析和转换结果为统一的 ViewDict 格式 views = [] if "bboxes" in result: # 打印模型识别出的元素总数(过滤前) total_detected = len(result["bboxes"]) print(f"[CV] 模型识别出的元素总数(过滤前): {total_detected}") # 打印各类型元素数量 icon_count = sum(1 for box in result["bboxes"] if box.get("type") == "icon") text_count = sum(1 for box in result["bboxes"] if box.get("type") == "text") print(f"[CV] - 图标元素数量: {icon_count}") print(f"[CV] - 文本元素数量: {text_count}") for idx, box_data in enumerate(result["bboxes"]): # 模型返回的归一化坐标 [x1, y1, x2, y2] bbox_norm = box_data.get("bbox") if not bbox_norm or len(bbox_norm) < 4: continue # 归一化坐标 x1_norm, y1_norm, x2_norm, y2_norm = bbox_norm # 转换为绝对像素坐标 x1 = int(x1_norm * width) y1 = int(y1_norm * height) x2 = int(x2_norm * width) y2 = int(y2_norm * height) # 过滤掉不合理的尺寸 if x2 <= x1 or y2 <= y1: continue # 获取文本信息(如果有) text = box_data.get("content", "") element_type = box_data.get("type", "cv_element") is_interactive = box_data.get("interactivity", True) # 按照 _get_view_signature 格式生成 signature,供 input_policy 使用 view_text_sig = text if text and len(text) <= 50 else "None" signature = "[class]%s[resource_id]%s[text]%s[%s,,]" % ( element_type or "None", "", # CV 视图无 resource_id view_text_sig or "None", "enabled", ) # 构建统一的 ViewDict 格式 view = { # === 必需字段 === "bounds": [[x1, y1], [x2, y2]], "text": text, "visible": True, "enabled": True, "clickable": is_interactive, "editable": False, "scrollable": False, "children": [], "view_str": f"cv_{idx}_{x1}_{y1}_{x2}_{y2}", "signature": signature, # === 可选字段 === "content_description": "", "resource_id": "", "class_name": element_type, "temp_id": idx, "parent": -1, "source": "cv", } views.append(view) return views def calculate_dhash(img): """ Calculate the dhash value of an image. :param img: numpy.ndarray, representing an image in opencv :return: """ difference = _calculate_pixel_difference(img) # convert to hex decimal_value = 0 hash_string = "" for index, value in enumerate(difference): if value: decimal_value += value * (2 ** (index % 8)) if index % 8 == 7: # every eight binary bit to one hex number hash_string += str(hex(decimal_value)[2:-1].rjust(2, "0")) # 0xf=>0x0f decimal_value = 0 return hash_string def _calculate_pixel_difference(img): """ Calculate difference between pixels :param img: numpy.ndarray, representing an image in opencv """ import cv2 resize_width = 18 resize_height = 16 # 1. resize to 18*16 smaller_image = cv2.resize(img, (resize_width, resize_height)) # 2. calculate grayscale grayscale_image = cv2.cvtColor(smaller_image, cv2.COLOR_BGR2GRAY) # 3. calculate difference between pixels difference = [] for row in range(resize_height): for col in range(resize_width - 1): difference.append(grayscale_image[row][col] > grayscale_image[row][col + 1]) return difference def dhash_hamming_distance(dhash1, dhash2): """ Calculate the hamming distance between two dhash values :param dhash1: str, the dhash of an image returned by `calculate_dhash` :param dhash2: str, the dhash of an image returned by `calculate_dhash` :return: int, the hamming distance between two dhash values """ difference = (int(dhash1, 16)) ^ (int(dhash2, 16)) return bin(difference).count("1")