feat(core): 支持等比例分辨率缩放
This commit is contained in:
parent
1ced1e3714
commit
bb7f6038a2
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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}')
|
|
@ -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:
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue