kotonebot 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/core.py +6 -49
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +7 -1
- kotonebot/client/device.py +108 -243
- kotonebot/client/host/__init__.py +34 -3
- kotonebot/client/host/adb_common.py +7 -9
- kotonebot/client/host/custom.py +6 -2
- kotonebot/client/host/leidian_host.py +2 -7
- kotonebot/client/host/mumu12_host.py +2 -7
- kotonebot/client/host/protocol.py +4 -3
- kotonebot/client/implements/__init__.py +62 -11
- kotonebot/client/implements/adb.py +5 -1
- kotonebot/client/implements/nemu_ipc/__init__.py +4 -0
- kotonebot/client/implements/uiautomator2.py +6 -2
- kotonebot/client/implements/windows.py +7 -3
- kotonebot/client/registration.py +1 -1
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +1 -1
- kotonebot/config/config.py +61 -0
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +7 -2
- kotonebot/interop/win/__init__.py +10 -1
- kotonebot/interop/win/_mouse.py +311 -0
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/primitives/__init__.py +3 -1
- kotonebot/primitives/geometry.py +817 -40
- kotonebot/primitives/visual.py +81 -1
- kotonebot/ui/pushkit/image_host.py +2 -1
- kotonebot/ui/pushkit/wxpusher.py +2 -1
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +4 -1
- kotonebot-0.6.0.dist-info/RECORD +105 -0
- kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
- kotonebot/client/implements/adb_raw.py +0 -159
- kotonebot-0.4.0.dist-info/RECORD +0 -70
- /kotonebot/{tools → devtools}/mirror.py +0 -0
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kotonebot-0.4.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -4,9 +4,6 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
from typing import Callable, TypeVar, Protocol, Any, Generic
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
|
|
7
|
-
from adbutils import adb, AdbTimeout, AdbError
|
|
8
|
-
from adbutils._device import AdbDevice
|
|
9
|
-
|
|
10
7
|
from kotonebot import logging
|
|
11
8
|
from kotonebot.client import Device, DeviceImpl
|
|
12
9
|
|
|
@@ -118,7 +115,11 @@ class Instance(Generic[T_HostConfig], ABC):
|
|
|
118
115
|
"""
|
|
119
116
|
raise NotImplementedError()
|
|
120
117
|
|
|
118
|
+
# TODO: [refactor] 这个方法不应该挂在 Instance,而是 AndroidEmulatorInstance 上
|
|
121
119
|
def wait_available(self, timeout: float = 180):
|
|
120
|
+
from adbutils import adb, AdbTimeout, AdbError
|
|
121
|
+
from adbutils._device import AdbDevice
|
|
122
|
+
|
|
122
123
|
logger.info('Starting to wait for emulator %s(127.0.0.1:%d) to be available...', self.name, self.adb_port)
|
|
123
124
|
state = 0
|
|
124
125
|
port = self.require_adb_port()
|
|
@@ -1,15 +1,66 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
1
2
|
from kotonebot.util import is_windows, require_windows
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
from . import
|
|
5
|
-
from . import
|
|
6
|
-
from . import
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .adb import AdbImpl, AdbImplConfig
|
|
6
|
+
from .uiautomator2 import UiAutomator2Impl
|
|
7
|
+
from .windows import WindowsImpl, WindowsImplConfig
|
|
8
|
+
from .remote_windows import RemoteWindowsImpl, RemoteWindowsImplConfig, RemoteWindowsServer
|
|
9
|
+
from .nemu_ipc import NemuIpcImpl, NemuIpcImplConfig, ExternalRendererIpc
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
|
|
12
|
+
def _require_windows():
|
|
13
|
+
global WindowsImpl, WindowsImplConfig
|
|
14
|
+
global RemoteWindowsImpl, RemoteWindowsImplConfig, RemoteWindowsServer
|
|
15
|
+
global NemuIpcImpl, NemuIpcImplConfig, ExternalRendererIpc
|
|
16
|
+
|
|
17
|
+
if not is_windows():
|
|
18
|
+
require_windows('"windows", "remote_windows" and "nemu_ipc" implementations')
|
|
19
|
+
from .windows import WindowsImpl, WindowsImplConfig
|
|
20
|
+
from .remote_windows import RemoteWindowsImpl, RemoteWindowsImplConfig, RemoteWindowsServer
|
|
21
|
+
from .nemu_ipc import NemuIpcImpl, NemuIpcImplConfig, ExternalRendererIpc
|
|
22
|
+
|
|
23
|
+
def _require_adb():
|
|
24
|
+
global AdbImpl, AdbImplConfig
|
|
25
|
+
|
|
26
|
+
from .adb import AdbImpl, AdbImplConfig
|
|
27
|
+
|
|
28
|
+
def _require_uiautomator2():
|
|
29
|
+
global UiAutomator2Impl
|
|
30
|
+
|
|
31
|
+
from .uiautomator2 import UiAutomator2Impl
|
|
32
|
+
|
|
33
|
+
_IMPORT_NAMES = [
|
|
34
|
+
(_require_windows, [
|
|
35
|
+
'WindowsImpl', 'WindowsImplConfig',
|
|
36
|
+
'RemoteWindowsImpl', 'RemoteWindowsImplConfig', 'RemoteWindowsServer',
|
|
37
|
+
'NemuIpcImpl', 'NemuIpcImplConfig', 'ExternalRendererIpc'
|
|
38
|
+
]),
|
|
39
|
+
(_require_adb, [
|
|
40
|
+
'AdbImpl', 'AdbImplConfig',
|
|
41
|
+
]),
|
|
42
|
+
(_require_uiautomator2, [
|
|
43
|
+
'UiAutomator2Impl'
|
|
44
|
+
]),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def __getattr__(name: str):
|
|
49
|
+
for item in _IMPORT_NAMES:
|
|
50
|
+
if name in item[1]:
|
|
51
|
+
item[0]()
|
|
52
|
+
break
|
|
10
53
|
try:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
54
|
+
return globals()[name]
|
|
55
|
+
except KeyError:
|
|
56
|
+
raise AttributeError(name=name)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# windows
|
|
60
|
+
'WindowsImpl', 'WindowsImplConfig',
|
|
61
|
+
'RemoteWindowsImpl', 'RemoteWindowsImplConfig', 'RemoteWindowsServer',
|
|
62
|
+
'NemuIpcImpl', 'NemuIpcImplConfig', 'ExternalRendererIpc',
|
|
63
|
+
# android
|
|
64
|
+
'AdbImpl', 'AdbImplConfig',
|
|
65
|
+
'UiAutomator2Impl'
|
|
66
|
+
]
|
|
@@ -5,7 +5,11 @@ from typing_extensions import override
|
|
|
5
5
|
import cv2
|
|
6
6
|
import numpy as np
|
|
7
7
|
from cv2.typing import MatLike
|
|
8
|
-
|
|
8
|
+
try:
|
|
9
|
+
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
10
|
+
except ImportError as _e:
|
|
11
|
+
from kotonebot.errors import MissingDependencyError
|
|
12
|
+
raise MissingDependencyError(_e, 'android')
|
|
9
13
|
|
|
10
14
|
from ..device import AndroidDevice
|
|
11
15
|
from ..protocol import AndroidCommandable, Touchable, Screenshotable
|
|
@@ -2,9 +2,13 @@ import time
|
|
|
2
2
|
from typing import Literal
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
-
|
|
5
|
+
try:
|
|
6
|
+
import uiautomator2 as u2
|
|
7
|
+
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
8
|
+
except ImportError as _e:
|
|
9
|
+
from kotonebot.errors import MissingDependencyError
|
|
10
|
+
raise MissingDependencyError(_e, 'android')
|
|
6
11
|
from cv2.typing import MatLike
|
|
7
|
-
from adbutils._device import AdbDevice as AdbUtilsDevice
|
|
8
12
|
|
|
9
13
|
from kotonebot import logging
|
|
10
14
|
from ..device import Device
|
|
@@ -9,10 +9,14 @@ from functools import cached_property
|
|
|
9
9
|
from dataclasses import dataclass
|
|
10
10
|
|
|
11
11
|
import cv2
|
|
12
|
-
import win32ui
|
|
13
|
-
import win32gui
|
|
14
12
|
import numpy as np
|
|
15
|
-
|
|
13
|
+
try:
|
|
14
|
+
import win32ui
|
|
15
|
+
import win32gui
|
|
16
|
+
from ahk import AHK, MsgBoxIcon
|
|
17
|
+
except ImportError as _e:
|
|
18
|
+
from kotonebot.errors import MissingDependencyError
|
|
19
|
+
raise MissingDependencyError(_e, 'windows')
|
|
16
20
|
from cv2.typing import MatLike
|
|
17
21
|
|
|
18
22
|
from ..device import Device, WindowsDevice
|
kotonebot/client/registration.py
CHANGED
|
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from .implements.windows import WindowsImplConfig
|
|
10
10
|
from .implements.nemu_ipc import NemuIpcImplConfig
|
|
11
11
|
|
|
12
|
-
AdbBasedImpl = Literal['adb', '
|
|
12
|
+
AdbBasedImpl = Literal['adb', 'uiautomator2']
|
|
13
13
|
DeviceImpl = str | AdbBasedImpl | Literal['windows', 'remote_windows', 'nemu_ipc']
|
|
14
14
|
|
|
15
15
|
# --- 核心类型定义 ---
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Any, overload
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
5
|
+
from cv2.typing import MatLike
|
|
6
|
+
|
|
7
|
+
from kotonebot.primitives.geometry import (
|
|
8
|
+
PointLike, RectLike, Size, SizeLike, AnyPointLike, unify_any_point
|
|
9
|
+
)
|
|
10
|
+
from kotonebot.primitives.geometry import (
|
|
11
|
+
is_point, is_point_f, is_rect, unify_rect,
|
|
12
|
+
Point, PointF, Rect
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
class AbstractScaler(ABC):
|
|
16
|
+
"""用于定义当实际设备分辨率与预期分辨率不一致时缩放行为的接口。
|
|
17
|
+
|
|
18
|
+
该接口定义了包括缩放图像、坐标转换、比例转换在内的方法。
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
# Accept either a `Size` instance or a plain (width, height) tuple.
|
|
22
|
+
self.physical_resolution: SizeLike | None = None
|
|
23
|
+
"""物理分辨率 (width, height)。"""
|
|
24
|
+
self.logic_resolution: SizeLike | None = None
|
|
25
|
+
"""逻辑分辨率 (width, height)。"""
|
|
26
|
+
|
|
27
|
+
def transform_screenshot(self, screenshot: MatLike) -> MatLike:
|
|
28
|
+
"""处理设备画面截图数据。
|
|
29
|
+
|
|
30
|
+
:param screenshot: 原始截图数据。
|
|
31
|
+
:return: 处理后的截图数据。
|
|
32
|
+
"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def logic_to_physical(self, v: AnyPointLike) -> AnyPointLike: ...
|
|
37
|
+
@overload
|
|
38
|
+
def logic_to_physical(self, v: RectLike) -> RectLike: ...
|
|
39
|
+
def logic_to_physical(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
40
|
+
"""将逻辑坐标转换为物理坐标。
|
|
41
|
+
|
|
42
|
+
:param v: 逻辑坐标点或矩形。
|
|
43
|
+
:return: 转换后的物理坐标点或矩形。
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
--------
|
|
47
|
+
>>> scaler.logic_to_physical(Point(10, 20))
|
|
48
|
+
<<< Point(..., ...)
|
|
49
|
+
>>> scaler.logic_to_physical(PointF(10.6, 20.5))
|
|
50
|
+
<<< Point(..., ...)
|
|
51
|
+
>>> scaler.logic_to_physical((10, 20))
|
|
52
|
+
<<< Point(..., ...)
|
|
53
|
+
>>> scaler.logic_to_physical(Rect(10, 20, 30, 40))
|
|
54
|
+
<<< Rect(..., ..., ..., ...)
|
|
55
|
+
>>> scaler.logic_to_physical((10, 20, 30, 40))
|
|
56
|
+
<<< Rect(..., ..., ..., ...)
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def physical_to_logic(self, v: PointLike) -> PointLike: ...
|
|
62
|
+
@overload
|
|
63
|
+
def physical_to_logic(self, v: RectLike) -> RectLike: ...
|
|
64
|
+
def physical_to_logic(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
65
|
+
"""将物理坐标转换为逻辑坐标。
|
|
66
|
+
|
|
67
|
+
:param v: 物理坐标点或矩形。
|
|
68
|
+
:return: 转换后的逻辑坐标点或矩形。
|
|
69
|
+
|
|
70
|
+
Examples
|
|
71
|
+
--------
|
|
72
|
+
见 :meth:`logic_to_physical`。
|
|
73
|
+
"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
@overload
|
|
77
|
+
def fractional_to_physical(self, v: PointLike) -> PointLike: ...
|
|
78
|
+
@overload
|
|
79
|
+
def fractional_to_physical(self, v: RectLike) -> RectLike: ...
|
|
80
|
+
def fractional_to_physical(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
81
|
+
"""将比例坐标转换为物理坐标。
|
|
82
|
+
|
|
83
|
+
:param v: 比例坐标点或矩形。
|
|
84
|
+
:return: 转换后的物理坐标点或矩形。
|
|
85
|
+
|
|
86
|
+
Examples
|
|
87
|
+
--------
|
|
88
|
+
见 :meth:`logic_to_physical`。
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
@overload
|
|
93
|
+
def physical_to_fractional(self, v: PointLike) -> PointLike: ...
|
|
94
|
+
@overload
|
|
95
|
+
def physical_to_fractional(self, v: RectLike) -> RectLike: ...
|
|
96
|
+
def physical_to_fractional(self, v: AnyPointLike | RectLike) -> AnyPointLike | RectLike | Any:
|
|
97
|
+
"""将物理坐标转换为比例坐标。
|
|
98
|
+
|
|
99
|
+
:param v: 物理坐标点或矩形。
|
|
100
|
+
:return: 转换后的比例坐标点或矩形。
|
|
101
|
+
|
|
102
|
+
Examples
|
|
103
|
+
--------
|
|
104
|
+
见 :meth:`logic_to_physical`。
|
|
105
|
+
"""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProportionalScaler(AbstractScaler):
|
|
110
|
+
"""等比例缩放。
|
|
111
|
+
|
|
112
|
+
支持在物理分辨率和逻辑分辨率之间进行等比例缩放转换。
|
|
113
|
+
仅支持等比例缩放,若无法等比例缩放,则会抛出异常。
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
match_rotation: bool = True,
|
|
119
|
+
aspect_ratio_tolerance: float = 0.1
|
|
120
|
+
):
|
|
121
|
+
"""初始化等比例缩放器。"""
|
|
122
|
+
super().__init__()
|
|
123
|
+
|
|
124
|
+
self.match_rotation = match_rotation
|
|
125
|
+
"""分辨率缩放是否自动匹配旋转。
|
|
126
|
+
当目标与真实分辨率的宽高比不一致时,是否允许通过旋转(交换宽高)后再进行匹配。
|
|
127
|
+
|
|
128
|
+
True 表示忽略方向差异,只要宽高比一致就视为可缩放;False 表示必须匹配旋转。
|
|
129
|
+
"""
|
|
130
|
+
self.aspect_ratio_tolerance = aspect_ratio_tolerance
|
|
131
|
+
"""宽高比容差阈值。
|
|
132
|
+
|
|
133
|
+
判断两分辨率宽高比差异是否接受的阈值。
|
|
134
|
+
该值越小,对比例一致性的要求越严格。默认为 0.1(即 10% 容差)。
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def scale_ratio(self) -> float:
|
|
139
|
+
"""获取物理分辨率相对于逻辑分辨率的缩放比例。
|
|
140
|
+
|
|
141
|
+
由于是等比例缩放,长宽的缩放比例应当一致(在容差范围内)。
|
|
142
|
+
"""
|
|
143
|
+
if self.physical_resolution is None:
|
|
144
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
145
|
+
if self.logic_resolution is None:
|
|
146
|
+
return 1.0
|
|
147
|
+
|
|
148
|
+
phy_w, phy_h = self.physical_resolution
|
|
149
|
+
log_w, log_h = self.logic_resolution
|
|
150
|
+
|
|
151
|
+
if self.match_rotation:
|
|
152
|
+
return max(phy_w, phy_h) / max(log_w, log_h)
|
|
153
|
+
|
|
154
|
+
return phy_w / log_w
|
|
155
|
+
|
|
156
|
+
def _aspect_ratio_compatible(
|
|
157
|
+
self, src_size: SizeLike, tgt_size: SizeLike
|
|
158
|
+
) -> bool:
|
|
159
|
+
"""判断两个尺寸在宽高比意义上是否兼容。
|
|
160
|
+
|
|
161
|
+
若 ``self.match_rotation`` 为 True,忽略方向(长边/短边)进行比较。
|
|
162
|
+
判断标准由 ``self.aspect_ratio_tolerance`` 决定(默认 0.1)。
|
|
163
|
+
"""
|
|
164
|
+
src_w, src_h = src_size
|
|
165
|
+
tgt_w, tgt_h = tgt_size
|
|
166
|
+
|
|
167
|
+
# 尺寸必须为正
|
|
168
|
+
if src_w <= 0 or src_h <= 0:
|
|
169
|
+
raise ValueError(f"Source size dimensions must be positive for scaling: {src_size}")
|
|
170
|
+
if tgt_w <= 0 or tgt_h <= 0:
|
|
171
|
+
raise ValueError(f"Target size dimensions must be positive for scaling: {tgt_size}")
|
|
172
|
+
|
|
173
|
+
tolerant = self.aspect_ratio_tolerance
|
|
174
|
+
|
|
175
|
+
# 直接比较宽高比
|
|
176
|
+
if abs((tgt_w / src_w) - (tgt_h / src_h)) <= tolerant:
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
# 尝试忽略方向差异
|
|
180
|
+
if self.match_rotation:
|
|
181
|
+
ratio_src = max(src_w, src_h) / min(src_w, src_h)
|
|
182
|
+
ratio_tgt = max(tgt_w, tgt_h) / min(tgt_w, tgt_h)
|
|
183
|
+
return abs(ratio_src - ratio_tgt) <= tolerant
|
|
184
|
+
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
def _assert_scalable(
|
|
188
|
+
self, source: SizeLike, target: SizeLike
|
|
189
|
+
) -> SizeLike:
|
|
190
|
+
"""校验分辨率是否可缩放,并返回调整后的目标分辨率。
|
|
191
|
+
|
|
192
|
+
当 match_rotation 为 True 且源分辨率与目标分辨率的旋转方向不一致时,
|
|
193
|
+
自动交换目标分辨率的宽高,使其与源分辨率的方向保持一致。
|
|
194
|
+
|
|
195
|
+
:param source: 源分辨率 (width, height)
|
|
196
|
+
:param target: 目标分辨率 (width, height)
|
|
197
|
+
:return: 调整后的目标分辨率 (width, height)
|
|
198
|
+
:raises UnscalableResolutionError: 若宽高比不兼容
|
|
199
|
+
"""
|
|
200
|
+
from ..errors import UnscalableResolutionError
|
|
201
|
+
|
|
202
|
+
# 智能调整目标分辨率方向
|
|
203
|
+
adjusted_tgt_size = target
|
|
204
|
+
if self.match_rotation:
|
|
205
|
+
src_w, src_h = source
|
|
206
|
+
tgt_w, tgt_h = target
|
|
207
|
+
|
|
208
|
+
# 判断源分辨率和目标分辨率的方向
|
|
209
|
+
src_is_landscape = src_w > src_h
|
|
210
|
+
tgt_is_landscape = tgt_w > tgt_h
|
|
211
|
+
|
|
212
|
+
# 如果方向不一致,交换目标分辨率的宽高
|
|
213
|
+
if src_is_landscape != tgt_is_landscape:
|
|
214
|
+
adjusted_tgt_size = Size(tgt_h, tgt_w)
|
|
215
|
+
|
|
216
|
+
# 校验调整后的分辨率是否兼容
|
|
217
|
+
if not self._aspect_ratio_compatible(source, adjusted_tgt_size):
|
|
218
|
+
raise UnscalableResolutionError(tuple(target), tuple(source))
|
|
219
|
+
|
|
220
|
+
return adjusted_tgt_size
|
|
221
|
+
|
|
222
|
+
def transform_screenshot(self, screenshot: MatLike) -> MatLike:
|
|
223
|
+
"""处理设备画面截图数据,将物理分辨率缩放到逻辑分辨率。
|
|
224
|
+
|
|
225
|
+
:param screenshot: 原始截图数据。
|
|
226
|
+
:return: 处理后的截图数据。
|
|
227
|
+
"""
|
|
228
|
+
import cv2
|
|
229
|
+
|
|
230
|
+
if self.logic_resolution is None:
|
|
231
|
+
return screenshot
|
|
232
|
+
|
|
233
|
+
target_w, target_h = self.logic_resolution
|
|
234
|
+
h, w = screenshot.shape[:2]
|
|
235
|
+
|
|
236
|
+
# 校验分辨率是否可缩放并获取调整后的目标分辨率
|
|
237
|
+
adjusted_target = self._assert_scalable(Size(w, h), Size(target_w, target_h))
|
|
238
|
+
|
|
239
|
+
return cv2.resize(screenshot, tuple(adjusted_target))
|
|
240
|
+
|
|
241
|
+
def logic_to_physical(self, v: AnyPointLike | RectLike) -> Any:
|
|
242
|
+
"""将逻辑坐标转换为物理坐标。
|
|
243
|
+
|
|
244
|
+
:param v: 逻辑坐标点或矩形。
|
|
245
|
+
:return: 转换后的物理坐标点或矩形。
|
|
246
|
+
"""
|
|
247
|
+
if self.physical_resolution is None:
|
|
248
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
249
|
+
if self.logic_resolution is None:
|
|
250
|
+
return v
|
|
251
|
+
|
|
252
|
+
# 校验分辨率是否可缩放
|
|
253
|
+
self._assert_scalable(self.logic_resolution, self.physical_resolution)
|
|
254
|
+
|
|
255
|
+
ratio = self.scale_ratio
|
|
256
|
+
|
|
257
|
+
# 处理点类型
|
|
258
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
259
|
+
point = unify_any_point(v)
|
|
260
|
+
|
|
261
|
+
new_x = point.x * ratio
|
|
262
|
+
new_y = point.y * ratio
|
|
263
|
+
|
|
264
|
+
if isinstance(point, PointF):
|
|
265
|
+
return PointF(new_x, new_y, name=point.name)
|
|
266
|
+
else:
|
|
267
|
+
return Point(int(new_x), int(new_y), name=point.name)
|
|
268
|
+
|
|
269
|
+
# 处理矩形类型
|
|
270
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
271
|
+
rect = unify_rect(v)
|
|
272
|
+
|
|
273
|
+
new_x = int(rect.x1 * ratio)
|
|
274
|
+
new_y = int(rect.y1 * ratio)
|
|
275
|
+
new_w = int(rect.w * ratio)
|
|
276
|
+
new_h = int(rect.h * ratio)
|
|
277
|
+
|
|
278
|
+
return Rect(new_x, new_y, new_w, new_h, name=rect.name)
|
|
279
|
+
|
|
280
|
+
return v
|
|
281
|
+
|
|
282
|
+
def physical_to_logic(self, v: AnyPointLike | RectLike) -> Any:
|
|
283
|
+
"""将物理坐标转换为逻辑坐标。
|
|
284
|
+
|
|
285
|
+
:param v: 物理坐标点或矩形。
|
|
286
|
+
:return: 转换后的逻辑坐标点或矩形。
|
|
287
|
+
"""
|
|
288
|
+
if self.physical_resolution is None:
|
|
289
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
290
|
+
if self.logic_resolution is None:
|
|
291
|
+
return v
|
|
292
|
+
|
|
293
|
+
# 校验分辨率是否可缩放
|
|
294
|
+
self._assert_scalable(self.logic_resolution, self.physical_resolution)
|
|
295
|
+
|
|
296
|
+
# 类型断言:如果 logic_resolution 不为 None,则 _adjusted_logic_resolution 也不为 None
|
|
297
|
+
assert self.logic_resolution is not None
|
|
298
|
+
|
|
299
|
+
ratio = self.scale_ratio
|
|
300
|
+
|
|
301
|
+
# 处理点类型
|
|
302
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
303
|
+
point = unify_any_point(v)
|
|
304
|
+
|
|
305
|
+
new_x = point.x / ratio
|
|
306
|
+
new_y = point.y / ratio
|
|
307
|
+
|
|
308
|
+
if isinstance(point, PointF):
|
|
309
|
+
return PointF(new_x, new_y, name=point.name)
|
|
310
|
+
else:
|
|
311
|
+
return Point(int(new_x), int(new_y), name=point.name)
|
|
312
|
+
|
|
313
|
+
# 处理矩形类型
|
|
314
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
315
|
+
rect = unify_rect(v)
|
|
316
|
+
|
|
317
|
+
new_x = int(rect.x1 / ratio)
|
|
318
|
+
new_y = int(rect.y1 / ratio)
|
|
319
|
+
new_w = int(rect.w / ratio)
|
|
320
|
+
new_h = int(rect.h / ratio)
|
|
321
|
+
|
|
322
|
+
return Rect(new_x, new_y, new_w, new_h, name=rect.name)
|
|
323
|
+
|
|
324
|
+
return v
|
|
325
|
+
|
|
326
|
+
def fractional_to_physical(self, v: AnyPointLike | RectLike) -> Any:
|
|
327
|
+
"""将比例坐标转换为物理坐标。
|
|
328
|
+
|
|
329
|
+
:param v: 比例坐标点或矩形(0-1范围)。
|
|
330
|
+
:return: 转换后的物理坐标点或矩形。
|
|
331
|
+
"""
|
|
332
|
+
if self.physical_resolution is None:
|
|
333
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
334
|
+
|
|
335
|
+
# 处理点类型
|
|
336
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
337
|
+
point = unify_any_point(v)
|
|
338
|
+
|
|
339
|
+
physical_w, physical_h = self.physical_resolution
|
|
340
|
+
|
|
341
|
+
new_x = point.x * physical_w
|
|
342
|
+
new_y = point.y * physical_h
|
|
343
|
+
|
|
344
|
+
if isinstance(point, PointF):
|
|
345
|
+
return PointF(new_x, new_y, name=point.name)
|
|
346
|
+
else:
|
|
347
|
+
return Point(int(new_x), int(new_y), name=point.name)
|
|
348
|
+
|
|
349
|
+
# 处理矩形类型
|
|
350
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
351
|
+
rect = unify_rect(v)
|
|
352
|
+
|
|
353
|
+
physical_w, physical_h = self.physical_resolution
|
|
354
|
+
|
|
355
|
+
new_x = int(rect.x1 * physical_w)
|
|
356
|
+
new_y = int(rect.y1 * physical_h)
|
|
357
|
+
new_w = int(rect.w * physical_w)
|
|
358
|
+
new_h = int(rect.h * physical_h)
|
|
359
|
+
|
|
360
|
+
return Rect(new_x, new_y, new_w, new_h, name=rect.name)
|
|
361
|
+
|
|
362
|
+
return v
|
|
363
|
+
|
|
364
|
+
def physical_to_fractional(self, v: AnyPointLike | RectLike) -> Any:
|
|
365
|
+
"""将物理坐标转换为比例坐标。
|
|
366
|
+
|
|
367
|
+
:param v: 物理坐标点或矩形。
|
|
368
|
+
:return: 转换后的比例坐标点或矩形(0-1范围)。
|
|
369
|
+
"""
|
|
370
|
+
if self.physical_resolution is None:
|
|
371
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
372
|
+
|
|
373
|
+
# 处理点类型
|
|
374
|
+
if is_point(v) or is_point_f(v) or (isinstance(v, tuple) and len(v) == 2):
|
|
375
|
+
point = unify_any_point(v)
|
|
376
|
+
|
|
377
|
+
physical_w, physical_h = self.physical_resolution
|
|
378
|
+
|
|
379
|
+
new_x = point.x / physical_w
|
|
380
|
+
new_y = point.y / physical_h
|
|
381
|
+
|
|
382
|
+
# 比例坐标总是返回 PointF
|
|
383
|
+
return PointF(new_x, new_y, name=point.name)
|
|
384
|
+
|
|
385
|
+
# 处理矩形类型
|
|
386
|
+
if is_rect(v) or (isinstance(v, tuple) and len(v) == 4):
|
|
387
|
+
rect = unify_rect(v)
|
|
388
|
+
|
|
389
|
+
physical_w, physical_h = self.physical_resolution
|
|
390
|
+
|
|
391
|
+
new_x = rect.x1 / physical_w
|
|
392
|
+
new_y = rect.y1 / physical_h
|
|
393
|
+
new_w = rect.w / physical_w
|
|
394
|
+
new_h = rect.h / physical_h
|
|
395
|
+
|
|
396
|
+
# 比例坐标的矩形需要转换为整数,但这里保持浮点精度
|
|
397
|
+
# 实际使用时可能需要根据具体需求调整
|
|
398
|
+
return Rect(int(new_x * 10000), int(new_y * 10000), int(new_w * 10000), int(new_h * 10000), name=rect.name)
|
|
399
|
+
|
|
400
|
+
return v
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class LandscapeGameScaler(ProportionalScaler):
|
|
404
|
+
"""横屏游戏等比例缩放。
|
|
405
|
+
|
|
406
|
+
对于横屏的游戏,通常若两个分辨率的长边一致,那么画面中元素大小也一致。
|
|
407
|
+
因此此缩放器会根据长边进行等比例缩放判断。
|
|
408
|
+
"""
|
|
409
|
+
def __init__(
|
|
410
|
+
self,
|
|
411
|
+
aspect_ratio_tolerance: float = 0.1
|
|
412
|
+
):
|
|
413
|
+
"""初始化横屏等比例缩放器。"""
|
|
414
|
+
super().__init__(
|
|
415
|
+
match_rotation=True,
|
|
416
|
+
aspect_ratio_tolerance=aspect_ratio_tolerance
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def scale_ratio(self) -> float:
|
|
421
|
+
if self.physical_resolution is None:
|
|
422
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
423
|
+
if self.logic_resolution is None:
|
|
424
|
+
return 1.0
|
|
425
|
+
|
|
426
|
+
# 横屏游戏根据长边(max)计算缩放比例
|
|
427
|
+
# Unpack explicitly to support both tuple and Vector2D/Size
|
|
428
|
+
phy_w, phy_h = self.physical_resolution
|
|
429
|
+
log_w, log_h = self.logic_resolution
|
|
430
|
+
|
|
431
|
+
return max(phy_w, phy_h) / max(log_w, log_h)
|
|
432
|
+
|
|
433
|
+
@override
|
|
434
|
+
def _assert_scalable(self, source: SizeLike, target: SizeLike) -> Size:
|
|
435
|
+
return Size(int(source[0] / self.scale_ratio), int(source[1] / self.scale_ratio))
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class PortraitGameScaler(ProportionalScaler):
|
|
439
|
+
"""竖屏游戏等比例缩放。
|
|
440
|
+
|
|
441
|
+
对于竖屏的游戏,通常以短边(宽度)为基准进行缩放。
|
|
442
|
+
"""
|
|
443
|
+
def __init__(
|
|
444
|
+
self,
|
|
445
|
+
aspect_ratio_tolerance: float = 0.1
|
|
446
|
+
):
|
|
447
|
+
"""初始化竖屏等比例缩放器。"""
|
|
448
|
+
super().__init__(
|
|
449
|
+
match_rotation=True,
|
|
450
|
+
aspect_ratio_tolerance=aspect_ratio_tolerance
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def scale_ratio(self) -> float:
|
|
455
|
+
if self.physical_resolution is None:
|
|
456
|
+
raise RuntimeError("Physical resolution is not set.")
|
|
457
|
+
if self.logic_resolution is None:
|
|
458
|
+
return 1.0
|
|
459
|
+
|
|
460
|
+
# 竖屏游戏根据短边(min)计算缩放比例
|
|
461
|
+
phy_w, phy_h = self.physical_resolution
|
|
462
|
+
log_w, log_h = self.logic_resolution
|
|
463
|
+
return min(phy_w, phy_h) / min(log_w, log_h)
|
|
464
|
+
|
|
465
|
+
@override
|
|
466
|
+
def _assert_scalable(self, source: SizeLike, target: SizeLike) -> Size:
|
|
467
|
+
return Size(int(source[0] / self.scale_ratio), int(source[1] / self.scale_ratio))
|
kotonebot/config/base_config.py
CHANGED
|
@@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict
|
|
|
6
6
|
|
|
7
7
|
T = TypeVar('T')
|
|
8
8
|
BackendType = Literal['custom', 'mumu12', 'mumu12v5', 'leidian', 'dmm']
|
|
9
|
-
DeviceRecipes = Literal['adb', '
|
|
9
|
+
DeviceRecipes = Literal['adb', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
|
|
10
10
|
|
|
11
11
|
class ConfigBaseModel(BaseModel):
|
|
12
12
|
model_config = ConfigDict(use_attribute_docstrings=True)
|