feat(core): 支持等比例分辨率缩放

This commit is contained in:
XcantloadX 2025-06-12 10:18:51 +08:00
parent 1ced1e3714
commit bb7f6038a2
4 changed files with 88 additions and 8 deletions

View File

@ -9,6 +9,7 @@ from cv2.typing import MatLike
from adbutils._device import AdbDevice as AdbUtilsDevice
from ..backend.debug import result
from ..errors import UnscalableResolutionError
from kotonebot.backend.core import HintBox
from kotonebot.primitives import Rect, Point, is_point
from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable, AndroidCommandable, WindowsCommandable
@ -78,6 +79,12 @@ class Device:
"""
设备平台名称
"""
self.target_resolution: tuple[int, int] | None = None
"""
目标分辨率
若设置则在截图点击滑动等时会缩放到目标分辨率
仅支持等比例缩放若无法等比例缩放则会抛出异常 `UnscalableResolutionError`
"""
@property
def adb(self) -> AdbUtilsDevice:
@ -89,6 +96,55 @@ class Device:
def adb(self, value: AdbUtilsDevice) -> None:
self._adb = value
def _scale_pos_real_to_target(self, real_x: int, real_y: int) -> tuple[int, int]:
"""将真实屏幕坐标缩放到目标逻辑坐标"""
if self.target_resolution is None:
return real_x, real_y
real_w, real_h = self.screen_size
target_w, target_h = self.target_resolution
if real_w <= 0 or real_h <= 0:
raise ValueError(f"Real screen size dimensions must be positive for scaling: {self.screen_size}")
# target_w 和 target_h 为0或负数的情况也应视为异常但UnscalableResolutionError主要关注比例
scale_w = target_w / real_w
scale_h = target_h / real_h
if abs(scale_w - scale_h) > 1e-9: # 使用容差比较浮点数
raise UnscalableResolutionError(self.target_resolution, self.screen_size)
return int(real_x * scale_w), int(real_y * scale_h)
def _scale_pos_target_to_real(self, target_x: int, target_y: int) -> tuple[int, int]:
"""将目标逻辑坐标缩放到真实屏幕坐标"""
if self.target_resolution is None:
return target_x, target_y # 输入坐标已是真实坐标
real_w, real_h = self.screen_size
target_w, target_h = self.target_resolution
if target_w <= 0 or target_h <= 0:
raise ValueError(f"Target resolution dimensions must be positive for scaling: {self.target_resolution}")
if real_w <= 0 or real_h <= 0:
raise ValueError(f"Real screen size dimensions must be positive for scaling: {self.screen_size}")
# 检查宽高比是否一致 (target_w / real_w vs target_h / real_h)
if abs((target_w / real_w) - (target_h / real_h)) > 1e-9:
raise UnscalableResolutionError(self.target_resolution, self.screen_size)
scale_to_real_w = real_w / target_w
scale_to_real_h = real_h / target_h
return int(target_x * scale_to_real_w), int(target_y * scale_to_real_h)
def __scale_image (self, img: MatLike) -> MatLike:
if self.target_resolution is None:
return img
return cv2.resize(img, self.target_resolution)
@overload
def click(self) -> None:
"""
@ -161,6 +217,9 @@ class Device:
logger.debug(f"Executing click hook before: ({x}, {y})")
x, y = hook(x, y)
logger.debug(f"Click hook before result: ({x}, {y})")
if self.target_resolution is not None:
# 输入坐标为逻辑坐标,需要转换为真实坐标
x, y = self._scale_pos_target_to_real(x, y)
logger.debug(f"Click: {x}, {y}")
from ..backend.context import ContextStackVars
if ContextStackVars.current() is not None:
@ -232,6 +291,10 @@ class Device:
"""
滑动屏幕
"""
if self.target_resolution is not None:
# 输入坐标为逻辑坐标,需要转换为真实坐标
x1, y1 = self._scale_pos_target_to_real(x1, y1)
x2, y2 = self._scale_pos_target_to_real(x2, y2)
self._touch.swipe(x1, y1, x2, y2, duration)
def swipe_scaled(self, x1: float, y1: float, x2: float, y2: float, duration: float|None = None) -> None:
@ -260,6 +323,8 @@ class Device:
img = self.screenshot_raw()
if self.screenshot_hook_after is not None:
img = self.screenshot_hook_after(img)
if self.target_resolution is not None:
img = self.__scale_image(img)
return img
def screenshot_raw(self) -> MatLike:
@ -296,8 +361,15 @@ class Device:
`self.orientation` 属性默认为竖屏如果需要自动检测
调用 `self.detect_orientation()` 方法
如果已知方向也可以直接设置 `self.orientation` 属性
即使设置了 `self.target_resolution`返回的分辨率仍然是真实分辨率
"""
return self._screenshot.screen_size
size = self._screenshot.screen_size
if self.orientation == 'landscape':
size = sorted(size, reverse=True)
else:
size = sorted(size, reverse=False)
return size[0], size[1]
def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
"""

View File

@ -4,6 +4,7 @@ from typing import Literal
import numpy as np
import uiautomator2 as u2
from cv2.typing import MatLike
from adbutils._device import AdbDevice as AdbUtilsDevice
from kotonebot import logging
from ..device import Device
@ -16,9 +17,8 @@ logger = logging.getLogger(__name__)
SCREENSHOT_INTERVAL = 0.2
class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
def __init__(self, device: Device):
self.device = device
self.u2_client = u2.Device(device.adb.serial)
def __init__(self, adb_connection: AdbUtilsDevice):
self.u2_client = u2.Device(adb_connection.serial)
self.__last_screenshot_time = 0
def screenshot(self) -> MatLike:
@ -40,10 +40,7 @@ class UiAutomator2Impl(Screenshotable, Commandable, Touchable):
def screen_size(self) -> tuple[int, int]:
info = self.u2_client.info
sizes = info['displayWidth'], info['displayHeight']
if self.device.orientation == 'landscape':
return (max(sizes), min(sizes))
else:
return (min(sizes), max(sizes))
return sizes
def detect_orientation(self) -> Literal['portrait', 'landscape'] | None:
"""

View File

@ -24,3 +24,10 @@ class TaskNotFoundError(KotonebotError):
def __init__(self, task_id: str):
self.task_id = task_id
super().__init__(f'Task "{task_id}" not found.')
class UnscalableResolutionError(KotonebotError):
def __init__(self, target_resolution: tuple[int, int], screen_size: tuple[int, int]):
self.target_resolution = target_resolution
self.screen_size = screen_size
super().__init__(f'Cannot scale to target resolution {target_resolution}. '
f'Screen size: {screen_size}')

View File

@ -112,6 +112,10 @@ class Kaa(KotoneBot):
if self.backend_instance is None:
raise ValueError('Backend instance is not set.')
_set_instance(self.backend_instance)
from kotonebot import device
logger.info('Set target resolution to 720x1280.')
device.orientation = 'portrait'
device.target_resolution = (720, 1280)
def __get_backend_instance(self, config: UserConfig) -> Instance:
"""