ascript 1.3.27__py3-none-win_amd64.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.
- ascript/__init__.py +8 -0
- ascript/hooks/__init__.py +0 -0
- ascript/hooks/hook-ascript.py +2 -0
- ascript/windows/__init__.py +29 -0
- ascript/windows/daos/__init__.py +0 -0
- ascript/windows/daos/screen.py +143 -0
- ascript/windows/hardware/__init__.py +5 -0
- ascript/windows/hardware/keyboard.py +279 -0
- ascript/windows/hardware/logitech_driver.py +179 -0
- ascript/windows/hardware/mouse.py +308 -0
- ascript/windows/license/__init__.py +16 -0
- ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
- ascript/windows/media/__init__.py +0 -0
- ascript/windows/resources/__init__.py +0 -0
- ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/templates/__init__.py +0 -0
- ascript/windows/resources/templates/__init__py.win.template +51 -0
- ascript/windows/resources/templates/build.as.win.template +14 -0
- ascript/windows/resources/templates/build.py +71 -0
- ascript/windows/resources/templates/logo_windows.ico +0 -0
- ascript/windows/resources/templates/windows/main.py +50 -0
- ascript/windows/resources/templates/windows/pyproject.toml +44 -0
- ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
- ascript/windows/screen/__init__.py +13 -0
- ascript/windows/screen/as_cv.py +382 -0
- ascript/windows/screen/canny.py +128 -0
- ascript/windows/screen/code_scanner.py +72 -0
- ascript/windows/screen/color_algorithms.py +277 -0
- ascript/windows/screen/find_colors.py +251 -0
- ascript/windows/screen/find_images.py +217 -0
- ascript/windows/screen/ocr/__init__.py +4 -0
- ascript/windows/screen/ocr/ocr.py +223 -0
- ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
- ascript/windows/screen/ocrx.py +169 -0
- ascript/windows/screen/screen_core.py +139 -0
- ascript/windows/store/__init__.py +21 -0
- ascript/windows/system/__init__.py +8 -0
- ascript/windows/system/device.py +355 -0
- ascript/windows/system/easy_print.py +115 -0
- ascript/windows/system/env.py +492 -0
- ascript/windows/system/exception/__init__.py +1 -0
- ascript/windows/system/key_value.py +145 -0
- ascript/windows/system/pip.py +175 -0
- ascript/windows/system/r.py +165 -0
- ascript/windows/system/runtime.py +62 -0
- ascript/windows/tools/__init__.py +27 -0
- ascript/windows/tools/__main__.py +21 -0
- ascript/windows/tools/action.json +33 -0
- ascript/windows/tools/action.py +168 -0
- ascript/windows/tools/colors_tool.py +391 -0
- ascript/windows/tools/config.py +26 -0
- ascript/windows/tools/dumptool.py +18 -0
- ascript/windows/tools/eel_api.py +495 -0
- ascript/windows/tools/gpplug/__init__.py +0 -0
- ascript/windows/tools/gpplug/plug.py +70 -0
- ascript/windows/tools/mouse_catch.py +168 -0
- ascript/windows/tools/plug_manager.py +38 -0
- ascript/windows/tools/tool_api.py +906 -0
- ascript/windows/tools/tool_window.py +56 -0
- ascript/windows/tools/uia_worker.py +365 -0
- ascript/windows/tools/web/app_store_window.html +481 -0
- ascript/windows/tools/web/colors.html +532 -0
- ascript/windows/tools/web/css/screen.css +1342 -0
- ascript/windows/tools/web/css/viewtree.css +357 -0
- ascript/windows/tools/web/flot_control.html +195 -0
- ascript/windows/tools/web/img/ico/favicon.png +0 -0
- ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
- ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
- ascript/windows/tools/web/img/svg/find_images.svg +1 -0
- ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
- ascript/windows/tools/web/img/svg/img_default.svg +1 -0
- ascript/windows/tools/web/img/svg/ocr.svg +1 -0
- ascript/windows/tools/web/js/screen/colors.js +466 -0
- ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
- ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
- ascript/windows/tools/web/js/screen/img_view.js +558 -0
- ascript/windows/tools/web/js/screen/plug.js +246 -0
- ascript/windows/tools/web/js/screen/screen.js +690 -0
- ascript/windows/tools/web/js/screen/screen_api.js +778 -0
- ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
- ascript/windows/tools/web/js/utils/map.js +73 -0
- ascript/windows/tools/web/js/utils/upload.js +157 -0
- ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
- ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
- ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
- ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
- ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
- ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
- ascript/windows/tools/web/libs/jquery/core.js +399 -0
- ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
- ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
- ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
- ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
- ascript/windows/tools/web/license_window.html +255 -0
- ascript/windows/tools/web/plugs/a.html +653 -0
- ascript/windows/tools/web/plugs/demo.html +52 -0
- ascript/windows/tools/web/plugs/find_colors.html +629 -0
- ascript/windows/tools/web/plugs/find_images.html +699 -0
- ascript/windows/tools/web/plugs/rapidocr.html +954 -0
- ascript/windows/tools/web/vtree.html +496 -0
- ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
- ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
- ascript/windows/tools/webview/index.html +13 -0
- ascript/windows/ui/__init__.py +6 -0
- ascript/windows/ui/app_store_window.py +272 -0
- ascript/windows/ui/draw_window.py +231 -0
- ascript/windows/ui/flot_test.py +297 -0
- ascript/windows/ui/flot_window.py +488 -0
- ascript/windows/ui/license_window.py +111 -0
- ascript/windows/ui/web_window.py +466 -0
- ascript/windows/utils/__init__.py +0 -0
- ascript/windows/utils/node_utils.py +20 -0
- ascript/windows/utils/screen_utils.py +280 -0
- ascript/windows/window/Selector.py +1015 -0
- ascript/windows/window/Window.py +747 -0
- ascript/windows/window/__init__.py +5 -0
- ascript/windows/window/launcher.py +213 -0
- ascript/windows/window/uia_runtime.py +26 -0
- ascript-1.3.27.dist-info/METADATA +41 -0
- ascript-1.3.27.dist-info/RECORD +177 -0
- ascript-1.3.27.dist-info/WHEEL +5 -0
- ascript-1.3.27.dist-info/entry_points.txt +2 -0
- ascript-1.3.27.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Iterable, List, Sequence, Tuple
|
|
3
|
+
|
|
4
|
+
import cv2
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .as_cv import AsCv
|
|
8
|
+
from ..daos.screen import Point
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ColorSpec = Tuple[np.ndarray, np.ndarray]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _source_image(image=None):
|
|
15
|
+
if image is None:
|
|
16
|
+
from .screen_core import Screen
|
|
17
|
+
image = Screen.capture()
|
|
18
|
+
result = AsCv.to_numpy(image)
|
|
19
|
+
if result is None or result.size == 0:
|
|
20
|
+
raise ValueError("图像为空")
|
|
21
|
+
if result.ndim == 2:
|
|
22
|
+
result = cv2.cvtColor(result, cv2.COLOR_GRAY2BGR)
|
|
23
|
+
if result.shape[2] == 4:
|
|
24
|
+
result = cv2.cvtColor(result, cv2.COLOR_BGRA2BGR)
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _crop(image: np.ndarray, rect=None):
|
|
29
|
+
height, width = image.shape[:2]
|
|
30
|
+
if not rect:
|
|
31
|
+
return image, 0, 0
|
|
32
|
+
left, top, right, bottom = (int(value) for value in rect)
|
|
33
|
+
left, right = sorted((max(0, left), min(width, right)))
|
|
34
|
+
top, bottom = sorted((max(0, top), min(height, bottom)))
|
|
35
|
+
if right <= left or bottom <= top:
|
|
36
|
+
raise ValueError("搜索区域无效")
|
|
37
|
+
return image[top:bottom, left:right], left, top
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _hex_bgr(value: str) -> np.ndarray:
|
|
41
|
+
value = value.strip().lstrip("#")
|
|
42
|
+
if len(value) == 3:
|
|
43
|
+
value = "".join(char * 2 for char in value)
|
|
44
|
+
if not re.fullmatch(r"[0-9a-fA-F]{6}", value):
|
|
45
|
+
raise ValueError(f"颜色格式错误: {value}")
|
|
46
|
+
red, green, blue = (int(value[index:index + 2], 16) for index in (0, 2, 4))
|
|
47
|
+
return np.array((blue, green, red), dtype=np.int16)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _split_color_specs(colors: str) -> Iterable[str]:
|
|
51
|
+
return (item.strip() for item in re.split(r"[|,]", colors or "") if item.strip())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_color_specs(colors: str, similarity: float = 1.0) -> List[ColorSpec]:
|
|
55
|
+
default_tolerance = max(0, min(255, round(255 * (1.0 - float(similarity)))))
|
|
56
|
+
specs: List[ColorSpec] = []
|
|
57
|
+
for item in _split_color_specs(colors):
|
|
58
|
+
target_text, separator, tolerance_text = item.partition("-")
|
|
59
|
+
target = _hex_bgr(target_text)
|
|
60
|
+
tolerance = (
|
|
61
|
+
_hex_bgr(tolerance_text)
|
|
62
|
+
if separator and tolerance_text.strip()
|
|
63
|
+
else np.full(3, default_tolerance, dtype=np.int16)
|
|
64
|
+
)
|
|
65
|
+
specs.append((target, tolerance))
|
|
66
|
+
if not specs:
|
|
67
|
+
raise ValueError("至少需要一个颜色")
|
|
68
|
+
return specs
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def color_mask(image, colors: str, rect=None, similarity: float = 1.0, mode: int = 0):
|
|
72
|
+
source, offset_x, offset_y = _crop(_source_image(image), rect)
|
|
73
|
+
pixels = source.astype(np.int16)
|
|
74
|
+
mask = np.zeros(source.shape[:2], dtype=bool)
|
|
75
|
+
for target, tolerance in parse_color_specs(colors, similarity):
|
|
76
|
+
delta = np.abs(pixels - target)
|
|
77
|
+
if int(mode) == 1:
|
|
78
|
+
limit = max(1.0, float(np.linalg.norm(tolerance)))
|
|
79
|
+
current = np.linalg.norm(delta, axis=2) <= limit
|
|
80
|
+
elif int(mode) == 2:
|
|
81
|
+
lab = cv2.cvtColor(source, cv2.COLOR_BGR2LAB).astype(np.int16)
|
|
82
|
+
target_lab = cv2.cvtColor(np.uint8([[target[::-1]]]), cv2.COLOR_RGB2LAB)[0, 0].astype(np.int16)
|
|
83
|
+
limit = max(1.0, float(np.linalg.norm(tolerance)))
|
|
84
|
+
current = np.linalg.norm(lab - target_lab, axis=2) <= limit
|
|
85
|
+
else:
|
|
86
|
+
current = np.all(delta <= tolerance, axis=2)
|
|
87
|
+
mask |= current
|
|
88
|
+
return mask, source, offset_x, offset_y
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Colors:
|
|
92
|
+
"""颜色数量统计与过滤,兼容 Android ``Colors`` API。"""
|
|
93
|
+
|
|
94
|
+
DRAW_THRESHOLD = 1
|
|
95
|
+
DRAW_BLACK_BG = 2
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def count(colors: str, rect=None, sim: float = 1.0, bitmap=None, image=None, mode: int = 0) -> int:
|
|
99
|
+
source = image if image is not None else bitmap
|
|
100
|
+
mask, _, _, _ = color_mask(source, colors, rect=rect, similarity=sim, mode=mode)
|
|
101
|
+
return int(np.count_nonzero(mask))
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def filter(colors: str, rect=None, sim: float = 1.0, image=None,
|
|
105
|
+
draw_mode: int = DRAW_THRESHOLD, mode: int = 0):
|
|
106
|
+
mask, source, offset_x, offset_y = color_mask(image, colors, rect=rect, similarity=sim, mode=mode)
|
|
107
|
+
if draw_mode == Colors.DRAW_BLACK_BG:
|
|
108
|
+
output = np.zeros_like(source)
|
|
109
|
+
output[mask] = source[mask]
|
|
110
|
+
else:
|
|
111
|
+
output = np.zeros_like(source)
|
|
112
|
+
output[mask] = 255
|
|
113
|
+
return output, int(np.count_nonzero(mask)), (offset_x, offset_y)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class CompareColors:
|
|
117
|
+
"""按绝对坐标比色,支持 RGB、归一化 RGB 和 Lab 距离。"""
|
|
118
|
+
|
|
119
|
+
RGB = 0
|
|
120
|
+
NORM = 1
|
|
121
|
+
LAB = 2
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _tolerance(diff):
|
|
125
|
+
if isinstance(diff, str):
|
|
126
|
+
value = diff.strip()
|
|
127
|
+
if value.startswith("#"):
|
|
128
|
+
return _hex_bgr(value)
|
|
129
|
+
parts = [part.strip() for part in value.strip("()[]").split(",") if part.strip()]
|
|
130
|
+
if len(parts) == 3:
|
|
131
|
+
red, green, blue = (max(0, min(255, int(part))) for part in parts)
|
|
132
|
+
return np.array((blue, green, red), dtype=np.int16)
|
|
133
|
+
if isinstance(diff, (tuple, list, np.ndarray)) and len(diff) == 3:
|
|
134
|
+
red, green, blue = (max(0, min(255, int(value))) for value in diff)
|
|
135
|
+
return np.array((blue, green, red), dtype=np.int16)
|
|
136
|
+
tolerance = max(0, min(255, round(255 * (1.0 - float(diff)))))
|
|
137
|
+
return np.full(3, tolerance, dtype=np.int16)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _parse(colors: str, diff):
|
|
141
|
+
default_tolerance = CompareColors._tolerance(diff)
|
|
142
|
+
points = []
|
|
143
|
+
for item in (part.strip() for part in (colors or "").split("|") if part.strip()):
|
|
144
|
+
fields = item.split(",", 2)
|
|
145
|
+
if len(fields) != 3:
|
|
146
|
+
raise ValueError(f"比色格式错误: {item}")
|
|
147
|
+
x, y = int(fields[0]), int(fields[1])
|
|
148
|
+
target, point_tolerance = parse_color_specs(fields[2], 1.0)[0]
|
|
149
|
+
if "-" not in fields[2]:
|
|
150
|
+
point_tolerance = default_tolerance
|
|
151
|
+
points.append((x, y, target, point_tolerance))
|
|
152
|
+
if not points:
|
|
153
|
+
raise ValueError("至少需要一个比色点")
|
|
154
|
+
return points
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def compare_detail(colors: str, diff=0.9, bitmap=None, mode: int = 0,
|
|
158
|
+
sim: float = 1.0, image=None):
|
|
159
|
+
source = _source_image(image if image is not None else bitmap)
|
|
160
|
+
height, width = source.shape[:2]
|
|
161
|
+
points = CompareColors._parse(colors, diff)
|
|
162
|
+
matches = []
|
|
163
|
+
for x, y, target, tolerance in points:
|
|
164
|
+
if x < 0 or y < 0 or x >= width or y >= height:
|
|
165
|
+
matches.append({"x": x, "y": y, "matched": False, "score": 0.0, "reason": "坐标越界"})
|
|
166
|
+
continue
|
|
167
|
+
pixel = source[y, x].astype(np.int16)
|
|
168
|
+
delta = np.abs(pixel - target)
|
|
169
|
+
if int(mode) == CompareColors.NORM:
|
|
170
|
+
distance = float(np.linalg.norm(delta))
|
|
171
|
+
limit = max(1.0, float(np.linalg.norm(tolerance)))
|
|
172
|
+
matched = distance <= limit
|
|
173
|
+
score = max(0.0, 1.0 - distance / 441.67295593)
|
|
174
|
+
elif int(mode) == CompareColors.LAB:
|
|
175
|
+
pixel_lab = cv2.cvtColor(np.uint8([[pixel]]), cv2.COLOR_BGR2LAB)[0, 0].astype(np.int16)
|
|
176
|
+
target_lab = cv2.cvtColor(np.uint8([[target]]), cv2.COLOR_BGR2LAB)[0, 0].astype(np.int16)
|
|
177
|
+
distance = float(np.linalg.norm(pixel_lab - target_lab))
|
|
178
|
+
limit = max(1.0, float(np.linalg.norm(tolerance)))
|
|
179
|
+
matched = distance <= limit
|
|
180
|
+
score = max(0.0, 1.0 - distance / 441.67295593)
|
|
181
|
+
else:
|
|
182
|
+
matched = bool(np.all(delta <= tolerance))
|
|
183
|
+
score = max(0.0, 1.0 - float(np.max(delta)) / 255.0)
|
|
184
|
+
matches.append({
|
|
185
|
+
"x": x,
|
|
186
|
+
"y": y,
|
|
187
|
+
"matched": matched,
|
|
188
|
+
"score": round(score, 4),
|
|
189
|
+
"expected": f"#{int(target[2]):02X}{int(target[1]):02X}{int(target[0]):02X}",
|
|
190
|
+
"actual": f"#{int(pixel[2]):02X}{int(pixel[1]):02X}{int(pixel[0]):02X}",
|
|
191
|
+
})
|
|
192
|
+
matched_count = sum(1 for item in matches if item["matched"])
|
|
193
|
+
score = matched_count / len(matches)
|
|
194
|
+
return {"matched": score >= float(sim), "score": round(score, 4), "points": matches}
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def compare(colors: str, diff=0.9, bitmap=None, mode: int = 0,
|
|
198
|
+
until: int = 0, sim: float = 1.0, image=None) -> bool:
|
|
199
|
+
del until
|
|
200
|
+
return bool(CompareColors.compare_detail(colors, diff, bitmap, mode, sim, image)["matched"])
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def compare_multi(colors_list: Sequence[str], diff: float = 0.9, mode: int = 0,
|
|
204
|
+
sim: float = 1.0, image=None) -> int:
|
|
205
|
+
for index, colors in enumerate(colors_list):
|
|
206
|
+
if CompareColors.compare(colors, diff=diff, mode=mode, sim=sim, image=image):
|
|
207
|
+
return index
|
|
208
|
+
return -1
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class FindBlock:
|
|
212
|
+
"""基于颜色掩码的滑动窗口和连通域找色块。"""
|
|
213
|
+
|
|
214
|
+
RGB = 0
|
|
215
|
+
NORM = 1
|
|
216
|
+
LAB = 2
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _matches(colors, rect, diff, mode, image):
|
|
220
|
+
return color_mask(image, colors, rect=rect, similarity=diff, mode=mode)
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def find_all(colors: str, width: int = 50, height: int = 50, count: int = 100,
|
|
224
|
+
rect=None, diff: float = 0.9, mode: int = 0, space: int = 0,
|
|
225
|
+
image=None, max_results: int = 200):
|
|
226
|
+
mask, _, offset_x, offset_y = FindBlock._matches(colors, rect, diff, mode, image)
|
|
227
|
+
block_width, block_height = max(1, int(width)), max(1, int(height))
|
|
228
|
+
if block_width > mask.shape[1] or block_height > mask.shape[0]:
|
|
229
|
+
return []
|
|
230
|
+
integral = cv2.integral(mask.astype(np.uint8), sdepth=cv2.CV_32S)
|
|
231
|
+
totals = (
|
|
232
|
+
integral[block_height:, block_width:]
|
|
233
|
+
- integral[:-block_height, block_width:]
|
|
234
|
+
- integral[block_height:, :-block_width]
|
|
235
|
+
+ integral[:-block_height, :-block_width]
|
|
236
|
+
)
|
|
237
|
+
ys, xs = np.where(totals >= max(1, int(count)))
|
|
238
|
+
if not len(xs):
|
|
239
|
+
return []
|
|
240
|
+
candidates = sorted(
|
|
241
|
+
((int(totals[y, x]), int(x), int(y)) for x, y in zip(xs, ys)),
|
|
242
|
+
key=lambda item: (-item[0], item[2], item[1]),
|
|
243
|
+
)
|
|
244
|
+
separation = max(int(space), min(block_width, block_height) // 2)
|
|
245
|
+
results = []
|
|
246
|
+
for matched, left, top in candidates:
|
|
247
|
+
center = Point(left + block_width // 2 + offset_x, top + block_height // 2 + offset_y)
|
|
248
|
+
if any(abs(center[0] - old[0]) <= separation and abs(center[1] - old[1]) <= separation for old in results):
|
|
249
|
+
continue
|
|
250
|
+
results.append(center)
|
|
251
|
+
if max_results > 0 and len(results) >= int(max_results):
|
|
252
|
+
break
|
|
253
|
+
return results
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def find(colors: str, width: int = 50, height: int = 50, count: int = 100,
|
|
257
|
+
rect=None, diff: float = 0.9, mode: int = 0, space: int = 0, image=None):
|
|
258
|
+
results = FindBlock.find_all(colors, width, height, count, rect, diff, mode, space, image, 1)
|
|
259
|
+
return results[0] if results else None
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def get_range(colors: str, rect=None, diff: float = 0.9, mode: int = 0,
|
|
263
|
+
min_area: int = 10, image=None):
|
|
264
|
+
mask, _, offset_x, offset_y = FindBlock._matches(colors, rect, diff, mode, image)
|
|
265
|
+
count, _, stats, _ = cv2.connectedComponentsWithStats(mask.astype(np.uint8), connectivity=8)
|
|
266
|
+
ranges = []
|
|
267
|
+
for index in range(1, count):
|
|
268
|
+
left, top, width, height, area = (int(value) for value in stats[index])
|
|
269
|
+
if area >= max(1, int(min_area)):
|
|
270
|
+
ranges.append([left + offset_x, top + offset_y, width, height])
|
|
271
|
+
ranges.sort(key=lambda item: (item[1], item[0]))
|
|
272
|
+
return ranges
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def count_block(colors: str, rect=None, diff: float = 0.9, mode: int = 0,
|
|
276
|
+
min_area: int = 10, image=None) -> int:
|
|
277
|
+
return len(FindBlock.get_range(colors, rect, diff, mode, min_area, image))
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .as_cv import AsCv
|
|
3
|
+
from ..daos.screen import Point
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FindColors:
|
|
7
|
+
"""多点找色工具类。
|
|
8
|
+
|
|
9
|
+
基于向量化位移过滤算法实现高性能多点颜色匹配。
|
|
10
|
+
支持自定义搜索区域、容差阈值、排序方向等参数。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
RGB = 0
|
|
14
|
+
NORM = 1
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def _hex_to_bgr(hex_str):
|
|
18
|
+
"""将十六进制颜色字符串转换为 OpenCV BGR 格式元组。
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
hex_str: 十六进制颜色字符串,如 '#FF0000' 或 'FF0000'。
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
tuple: BGR 格式颜色元组,如 (0, 0, 255) 表示红色。
|
|
25
|
+
"""
|
|
26
|
+
hex_str = hex_str.lstrip('#')
|
|
27
|
+
r, g, b = tuple(int(hex_str[i:i + 2], 16) for i in (0, 2, 4))
|
|
28
|
+
return (b, g, r)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _parse_colors(colors_str, default_tolerance):
|
|
32
|
+
"""解析多点找色的颜色描述字符串。
|
|
33
|
+
|
|
34
|
+
将形如 "x,y,color|dx,dy,color" 的字符串解析为基准色和偏移点列表。
|
|
35
|
+
第一个点作为基准点,后续点记录相对于基准点的偏移和目标颜色。
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
colors_str: 颜色描述字符串,格式为 "x,y,#RRGGBB|x,y,#RRGGBB|..."。
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
tuple: (base_bgr, offsets),其中:
|
|
42
|
+
- base_bgr (numpy.ndarray): 基准点的 BGR 颜色数组。
|
|
43
|
+
- offsets (list): 偏移点列表,每项为 (dy, dx, target_bgr) 元组。
|
|
44
|
+
"""
|
|
45
|
+
parts = colors_str.split('|')
|
|
46
|
+
points = []
|
|
47
|
+
for p in parts:
|
|
48
|
+
x, y, color_and_diff = p.split(',', 2)
|
|
49
|
+
color, separator, point_diff = color_and_diff.partition('-')
|
|
50
|
+
tolerance = (
|
|
51
|
+
np.array(FindColors._hex_to_bgr(point_diff), dtype=np.int16)
|
|
52
|
+
if separator and point_diff.strip()
|
|
53
|
+
else default_tolerance
|
|
54
|
+
)
|
|
55
|
+
points.append((int(x), int(y), FindColors._hex_to_bgr(color), tolerance))
|
|
56
|
+
|
|
57
|
+
base_x, base_y, base_bgr, base_tolerance = points[0]
|
|
58
|
+
offsets = []
|
|
59
|
+
for i in range(1, len(points)):
|
|
60
|
+
curr_x, curr_y, curr_bgr, curr_tolerance = points[i]
|
|
61
|
+
# 计算相对于第一个点的偏移和对应的颜色数组
|
|
62
|
+
offsets.append((
|
|
63
|
+
curr_y - base_y,
|
|
64
|
+
curr_x - base_x,
|
|
65
|
+
np.array(curr_bgr, dtype=np.int16),
|
|
66
|
+
curr_tolerance,
|
|
67
|
+
))
|
|
68
|
+
|
|
69
|
+
return np.array(base_bgr, dtype=np.int16), base_tolerance, offsets
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def find(colors: str, rect=None, threshold: float = 0.98, ori: int = 2, exclusion_radius: int = 5, im_source=None,
|
|
73
|
+
max_res: int = 1, **kwargs):
|
|
74
|
+
"""查找第一个匹配的颜色目标。
|
|
75
|
+
|
|
76
|
+
对 find_all 的便捷封装,默认只返回第一个匹配结果。
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
colors: 多点颜色描述字符串,格式为 "x,y,#RRGGBB|x,y,#RRGGBB|..."。
|
|
80
|
+
rect: 搜索区域 [left, top, right, bottom],为 None 时搜索全屏。
|
|
81
|
+
threshold: 颜色匹配精度,范围 0.0 ~ 1.0,默认与 Android 一致为 0.98。
|
|
82
|
+
ori: 搜索方向(1-8),控制结果的排序优先级,默认为 2(从上到下、从左到右)。
|
|
83
|
+
exclusion_radius: 排除半径(像素),同一区域内不重复返回,默认为 5。
|
|
84
|
+
im_source: 自定义图像源,为 None 时自动截取当前屏幕。
|
|
85
|
+
max_res: 最大返回结果数,默认为 1。
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Point: 与 Android 一致的坐标对象,可通过 ``.x`` / ``.y`` 访问;未找到返回 None。
|
|
89
|
+
"""
|
|
90
|
+
res = FindColors.find_all(
|
|
91
|
+
colors, rect, threshold, ori, exclusion_radius, im_source, max_res=max_res, **kwargs
|
|
92
|
+
)
|
|
93
|
+
return res[0] if res else None
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def find_all(colors: str, rect=None, threshold: float = 0.98, ori: int = 2, exclusion_radius: int = 5,
|
|
97
|
+
im_source=None, max_res: int = 0, **kwargs):
|
|
98
|
+
"""全量查找所有匹配的颜色目标。
|
|
99
|
+
|
|
100
|
+
采用向量化位移过滤算法,通过 NumPy 整体矩阵运算实现高性能匹配,
|
|
101
|
+
避免逐像素 Python 循环导致的性能瓶颈。内置熔断机制防止海量结果卡死。
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
colors: 多点颜色描述字符串,格式为 "x,y,#RRGGBB|x,y,#RRGGBB|..."。
|
|
105
|
+
rect: 搜索区域 [left, top, right, bottom],为 None 时搜索全屏。
|
|
106
|
+
threshold: 颜色匹配精度,范围 0.0 ~ 1.0,默认与 Android 一致为 0.98。
|
|
107
|
+
容差计算公式:tol = 255 * (1.0 - threshold)。
|
|
108
|
+
ori: 搜索方向(1-8),控制结果排序:
|
|
109
|
+
1=左到右/上到下,2=上到下/左到右,3=上到下/右到左,
|
|
110
|
+
4=右到左/上到下,5=右到左/下到上,6=下到上/右到左,
|
|
111
|
+
7=下到上/左到右,8=左到右/下到上。默认为 2。
|
|
112
|
+
exclusion_radius: 排除半径(像素),同一区域内不重复返回,默认为 5。
|
|
113
|
+
im_source: 自定义图像源,为 None 时自动截取当前屏幕。
|
|
114
|
+
max_res: 最大返回结果数,0 表示不限制。
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
list: 与 Android 一致的 Point 坐标对象列表。
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
threshold = float(kwargs.pop("diff", threshold))
|
|
121
|
+
exclusion_radius = int(kwargs.pop("space", exclusion_radius))
|
|
122
|
+
im_source = kwargs.pop("image", im_source)
|
|
123
|
+
max_res = int(kwargs.pop("p_max_num", max_res))
|
|
124
|
+
mode = int(kwargs.pop("mode", 0))
|
|
125
|
+
kwargs.pop("until", None)
|
|
126
|
+
if kwargs:
|
|
127
|
+
raise TypeError(f"不支持的参数: {', '.join(kwargs)}")
|
|
128
|
+
|
|
129
|
+
# 获取图像源
|
|
130
|
+
if im_source is None:
|
|
131
|
+
from .screen_core import Screen
|
|
132
|
+
im_source = Screen.capture()
|
|
133
|
+
|
|
134
|
+
img = AsCv.to_numpy(im_source)
|
|
135
|
+
|
|
136
|
+
# 处理范围(ROI)
|
|
137
|
+
if rect:
|
|
138
|
+
l, t, r, b = rect
|
|
139
|
+
img = img[t:b, l:r]
|
|
140
|
+
offset_l, offset_t = l, t
|
|
141
|
+
else:
|
|
142
|
+
offset_l, offset_t = 0, 0
|
|
143
|
+
|
|
144
|
+
h, w, _ = img.shape
|
|
145
|
+
tol = int(255 * (1.0 - threshold))
|
|
146
|
+
default_tolerance = np.full(3, tol, dtype=np.int16)
|
|
147
|
+
base_bgr, base_tolerance, offsets = FindColors._parse_colors(colors, default_tolerance)
|
|
148
|
+
|
|
149
|
+
# 预转换图像类型,避免在循环中重复计算
|
|
150
|
+
img_int16 = img.astype(np.int16)
|
|
151
|
+
|
|
152
|
+
def matches(pixels, target, tolerance):
|
|
153
|
+
if mode == 1:
|
|
154
|
+
pixels_float = pixels.astype(np.float32)
|
|
155
|
+
target_float = target.astype(np.float32)
|
|
156
|
+
pixel_norm = np.linalg.norm(pixels_float, axis=-1)
|
|
157
|
+
target_norm = float(np.linalg.norm(target_float))
|
|
158
|
+
denominator = np.maximum(pixel_norm * max(target_norm, 1e-6), 1e-6)
|
|
159
|
+
cosine = np.sum(pixels_float * target_float, axis=-1) / denominator
|
|
160
|
+
return cosine >= threshold
|
|
161
|
+
return np.all(np.abs(pixels - target) <= tolerance, axis=-1)
|
|
162
|
+
|
|
163
|
+
# --- 步骤 1: 生成主色掩码 ---
|
|
164
|
+
final_mask = matches(img_int16, base_bgr, base_tolerance)
|
|
165
|
+
|
|
166
|
+
# 如果没有匹配到主色,直接返回
|
|
167
|
+
if not np.any(final_mask):
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
# --- 步骤 2: 向量化位移过滤 (核心防卡死逻辑) ---
|
|
171
|
+
for dy, dx, target_bgr, point_tolerance in offsets:
|
|
172
|
+
# 计算有效区域(防止偏移后越界)
|
|
173
|
+
y_start, y_end = max(0, dy), min(h, h + dy)
|
|
174
|
+
x_start, x_end = max(0, dx), min(w, w + dx)
|
|
175
|
+
oy_start, oy_end = max(0, -dy), min(h, h - dy)
|
|
176
|
+
ox_start, ox_end = max(0, -dx), min(w, w - dx)
|
|
177
|
+
|
|
178
|
+
if oy_start >= oy_end or ox_start >= ox_end:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# 核心:直接对整张掩码进行位移比对
|
|
182
|
+
# 这一步在 NumPy 内部完成,比 Python 循环快几个量级
|
|
183
|
+
shifted_pixels = img_int16[y_start:y_end, x_start:x_end]
|
|
184
|
+
match_offset = matches(shifted_pixels, target_bgr, point_tolerance)
|
|
185
|
+
|
|
186
|
+
# 对齐掩码并做逻辑“与”操作
|
|
187
|
+
aligned_mask = np.zeros((h, w), dtype=bool)
|
|
188
|
+
aligned_mask[oy_start:oy_end, ox_start:ox_end] = match_offset
|
|
189
|
+
|
|
190
|
+
final_mask &= aligned_mask
|
|
191
|
+
|
|
192
|
+
# 如果中间过程掩码已空,提前终止
|
|
193
|
+
if not np.any(final_mask):
|
|
194
|
+
return []
|
|
195
|
+
|
|
196
|
+
# --- 步骤 3: 提取剩下的合格点 ---
|
|
197
|
+
candidates = np.argwhere(final_mask)
|
|
198
|
+
|
|
199
|
+
# --- 步骤 4: 排序 (根据 ori 参数) ---
|
|
200
|
+
if len(candidates) > 1:
|
|
201
|
+
# 按照你原代码的 1-8 方向逻辑
|
|
202
|
+
# lexsort 是从后往前作为优先级排序的
|
|
203
|
+
y_values, x_values = candidates[:, 0], candidates[:, 1]
|
|
204
|
+
order_map = {
|
|
205
|
+
1: (y_values, x_values), # 从左到右, 从上到下
|
|
206
|
+
2: (x_values, y_values), # 从上到下, 从左到右
|
|
207
|
+
3: (-x_values, y_values), # 从上到下, 从右到左
|
|
208
|
+
4: (y_values, -x_values), # 从右到左, 从上到下
|
|
209
|
+
5: (-y_values, -x_values), # 从右到左, 从下到上
|
|
210
|
+
6: (-x_values, -y_values), # 从下到上, 从右到左
|
|
211
|
+
7: (x_values, -y_values), # 从下到上, 从左到右
|
|
212
|
+
8: (-y_values, x_values), # 从左到右, 从下到上
|
|
213
|
+
}
|
|
214
|
+
candidates = candidates[np.lexsort(order_map.get(ori, order_map[2]))]
|
|
215
|
+
|
|
216
|
+
# --- 步骤 5: 结果构建与排除半径处理 ---
|
|
217
|
+
results = []
|
|
218
|
+
if exclusion_radius > 0:
|
|
219
|
+
excluded = np.zeros((h, w), dtype=bool)
|
|
220
|
+
for y, x in candidates:
|
|
221
|
+
if excluded[y, x]:
|
|
222
|
+
continue
|
|
223
|
+
results.append(Point(int(x + offset_l), int(y + offset_t)))
|
|
224
|
+
if max_res > 0 and len(results) >= max_res:
|
|
225
|
+
break
|
|
226
|
+
# 标记半径内的点为已排除
|
|
227
|
+
y1, y2 = max(0, y - exclusion_radius), min(h, y + exclusion_radius)
|
|
228
|
+
x1, x2 = max(0, x - exclusion_radius), min(w, x + exclusion_radius)
|
|
229
|
+
excluded[y1:y2, x1:x2] = True
|
|
230
|
+
else:
|
|
231
|
+
for y, x in candidates:
|
|
232
|
+
results.append(Point(int(x + offset_l), int(y + offset_t)))
|
|
233
|
+
if max_res > 0 and len(results) >= max_res:
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
return results
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def find_color(color: str, rect=None, diff: float = 0.98, mode: int = 0):
|
|
240
|
+
"""Android-compatible single-color lookup returning ``{"x", "y"}``."""
|
|
241
|
+
result = FindColors.find(f"0,0,{color}", rect=rect, diff=diff, mode=mode)
|
|
242
|
+
return result.to_dict() if result is not None else None
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def find_all_color(color: str, rect=None, diff: float = 0.98, mode: int = 0,
|
|
246
|
+
max_num: int = 999999):
|
|
247
|
+
"""Android-compatible single-color lookup returning dictionaries."""
|
|
248
|
+
results = FindColors.find_all(
|
|
249
|
+
f"0,0,{color}", rect=rect, diff=diff, mode=mode, p_max_num=max_num
|
|
250
|
+
)
|
|
251
|
+
return [point.to_dict() for point in results] if results else None
|