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.
Files changed (177) hide show
  1. ascript/__init__.py +8 -0
  2. ascript/hooks/__init__.py +0 -0
  3. ascript/hooks/hook-ascript.py +2 -0
  4. ascript/windows/__init__.py +29 -0
  5. ascript/windows/daos/__init__.py +0 -0
  6. ascript/windows/daos/screen.py +143 -0
  7. ascript/windows/hardware/__init__.py +5 -0
  8. ascript/windows/hardware/keyboard.py +279 -0
  9. ascript/windows/hardware/logitech_driver.py +179 -0
  10. ascript/windows/hardware/mouse.py +308 -0
  11. ascript/windows/license/__init__.py +16 -0
  12. ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
  13. ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
  14. ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
  15. ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
  16. ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
  17. ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
  18. ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
  19. ascript/windows/media/__init__.py +0 -0
  20. ascript/windows/resources/__init__.py +0 -0
  21. ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
  22. ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
  23. ascript/windows/resources/templates/__init__.py +0 -0
  24. ascript/windows/resources/templates/__init__py.win.template +51 -0
  25. ascript/windows/resources/templates/build.as.win.template +14 -0
  26. ascript/windows/resources/templates/build.py +71 -0
  27. ascript/windows/resources/templates/logo_windows.ico +0 -0
  28. ascript/windows/resources/templates/windows/main.py +50 -0
  29. ascript/windows/resources/templates/windows/pyproject.toml +44 -0
  30. ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
  31. ascript/windows/screen/__init__.py +13 -0
  32. ascript/windows/screen/as_cv.py +382 -0
  33. ascript/windows/screen/canny.py +128 -0
  34. ascript/windows/screen/code_scanner.py +72 -0
  35. ascript/windows/screen/color_algorithms.py +277 -0
  36. ascript/windows/screen/find_colors.py +251 -0
  37. ascript/windows/screen/find_images.py +217 -0
  38. ascript/windows/screen/ocr/__init__.py +4 -0
  39. ascript/windows/screen/ocr/ocr.py +223 -0
  40. ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
  41. ascript/windows/screen/ocrx.py +169 -0
  42. ascript/windows/screen/screen_core.py +139 -0
  43. ascript/windows/store/__init__.py +21 -0
  44. ascript/windows/system/__init__.py +8 -0
  45. ascript/windows/system/device.py +355 -0
  46. ascript/windows/system/easy_print.py +115 -0
  47. ascript/windows/system/env.py +492 -0
  48. ascript/windows/system/exception/__init__.py +1 -0
  49. ascript/windows/system/key_value.py +145 -0
  50. ascript/windows/system/pip.py +175 -0
  51. ascript/windows/system/r.py +165 -0
  52. ascript/windows/system/runtime.py +62 -0
  53. ascript/windows/tools/__init__.py +27 -0
  54. ascript/windows/tools/__main__.py +21 -0
  55. ascript/windows/tools/action.json +33 -0
  56. ascript/windows/tools/action.py +168 -0
  57. ascript/windows/tools/colors_tool.py +391 -0
  58. ascript/windows/tools/config.py +26 -0
  59. ascript/windows/tools/dumptool.py +18 -0
  60. ascript/windows/tools/eel_api.py +495 -0
  61. ascript/windows/tools/gpplug/__init__.py +0 -0
  62. ascript/windows/tools/gpplug/plug.py +70 -0
  63. ascript/windows/tools/mouse_catch.py +168 -0
  64. ascript/windows/tools/plug_manager.py +38 -0
  65. ascript/windows/tools/tool_api.py +906 -0
  66. ascript/windows/tools/tool_window.py +56 -0
  67. ascript/windows/tools/uia_worker.py +365 -0
  68. ascript/windows/tools/web/app_store_window.html +481 -0
  69. ascript/windows/tools/web/colors.html +532 -0
  70. ascript/windows/tools/web/css/screen.css +1342 -0
  71. ascript/windows/tools/web/css/viewtree.css +357 -0
  72. ascript/windows/tools/web/flot_control.html +195 -0
  73. ascript/windows/tools/web/img/ico/favicon.png +0 -0
  74. ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
  75. ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
  76. ascript/windows/tools/web/img/svg/find_images.svg +1 -0
  77. ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
  78. ascript/windows/tools/web/img/svg/img_default.svg +1 -0
  79. ascript/windows/tools/web/img/svg/ocr.svg +1 -0
  80. ascript/windows/tools/web/js/screen/colors.js +466 -0
  81. ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
  82. ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
  83. ascript/windows/tools/web/js/screen/img_view.js +558 -0
  84. ascript/windows/tools/web/js/screen/plug.js +246 -0
  85. ascript/windows/tools/web/js/screen/screen.js +690 -0
  86. ascript/windows/tools/web/js/screen/screen_api.js +778 -0
  87. ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
  88. ascript/windows/tools/web/js/utils/map.js +73 -0
  89. ascript/windows/tools/web/js/utils/upload.js +157 -0
  90. ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
  91. ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
  92. ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
  93. ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
  94. ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
  95. ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
  96. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
  97. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
  98. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
  99. ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
  100. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
  101. ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
  102. ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
  103. ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
  104. ascript/windows/tools/web/libs/jquery/core.js +399 -0
  105. ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
  106. ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
  107. ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
  108. ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
  109. ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
  110. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
  111. ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
  112. ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
  113. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
  114. ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
  115. ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
  116. ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
  117. ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
  118. ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
  119. ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
  120. ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
  121. ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
  122. ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
  123. ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
  124. ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
  125. ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
  126. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
  127. ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
  128. ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
  129. ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
  130. ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
  131. ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
  132. ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
  133. ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
  134. ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
  135. ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
  136. ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
  137. ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
  138. ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
  139. ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
  140. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
  141. ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
  142. ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
  143. ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
  144. ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
  145. ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
  146. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
  147. ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
  148. ascript/windows/tools/web/license_window.html +255 -0
  149. ascript/windows/tools/web/plugs/a.html +653 -0
  150. ascript/windows/tools/web/plugs/demo.html +52 -0
  151. ascript/windows/tools/web/plugs/find_colors.html +629 -0
  152. ascript/windows/tools/web/plugs/find_images.html +699 -0
  153. ascript/windows/tools/web/plugs/rapidocr.html +954 -0
  154. ascript/windows/tools/web/vtree.html +496 -0
  155. ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
  156. ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
  157. ascript/windows/tools/webview/index.html +13 -0
  158. ascript/windows/ui/__init__.py +6 -0
  159. ascript/windows/ui/app_store_window.py +272 -0
  160. ascript/windows/ui/draw_window.py +231 -0
  161. ascript/windows/ui/flot_test.py +297 -0
  162. ascript/windows/ui/flot_window.py +488 -0
  163. ascript/windows/ui/license_window.py +111 -0
  164. ascript/windows/ui/web_window.py +466 -0
  165. ascript/windows/utils/__init__.py +0 -0
  166. ascript/windows/utils/node_utils.py +20 -0
  167. ascript/windows/utils/screen_utils.py +280 -0
  168. ascript/windows/window/Selector.py +1015 -0
  169. ascript/windows/window/Window.py +747 -0
  170. ascript/windows/window/__init__.py +5 -0
  171. ascript/windows/window/launcher.py +213 -0
  172. ascript/windows/window/uia_runtime.py +26 -0
  173. ascript-1.3.27.dist-info/METADATA +41 -0
  174. ascript-1.3.27.dist-info/RECORD +177 -0
  175. ascript-1.3.27.dist-info/WHEEL +5 -0
  176. ascript-1.3.27.dist-info/entry_points.txt +2 -0
  177. ascript-1.3.27.dist-info/top_level.txt +1 -0
@@ -0,0 +1,382 @@
1
+ import cv2
2
+ import numpy as np
3
+ import os
4
+ from PIL import Image
5
+ from dataclasses import dataclass, asdict
6
+ from typing import Tuple, List, Optional
7
+
8
+
9
+ @dataclass
10
+ class MatchResult:
11
+ """图像匹配结果的数据封装类。
12
+
13
+ 封装单次图像匹配的完整结果信息,包含坐标、置信度、匹配方法等。
14
+ 支持字典式访问和 JSON 序列化。
15
+
16
+ Attributes:
17
+ center: 匹配区域的中心点坐标,格式为 (x, y)。
18
+ rectangle: 匹配区域的四个顶点坐标列表,格式为 [(x1,y1), (x2,y2), (x3,y3), (x4,y4)]。
19
+ rect: 匹配区域的外接矩形,格式为 [left, top, right, bottom]。
20
+ confidence: 匹配置信度,范围 0.0 ~ 1.0。
21
+ method: 使用的匹配算法名称(如 'template_fast'、'multi_scale'、'akaze')。
22
+ scale: 匹配时的缩放比例,默认为 1.0。
23
+ """
24
+
25
+ center: Tuple[int, int]
26
+ rectangle: List[Tuple[int, int]]
27
+ rect: List[int]
28
+ confidence: float
29
+ method: str
30
+ scale: float = 1.0
31
+ template: Optional[str] = None
32
+ index: int = 0
33
+
34
+ @property
35
+ def x(self) -> int:
36
+ """获取匹配中心点的 X 坐标。
37
+
38
+ Returns:
39
+ int: 中心点 X 坐标值。
40
+ """
41
+ return self.center[0]
42
+
43
+ @property
44
+ def y(self) -> int:
45
+ """获取匹配中心点的 Y 坐标。
46
+
47
+ Returns:
48
+ int: 中心点 Y 坐标值。
49
+ """
50
+ return self.center[1]
51
+
52
+ @property
53
+ def result(self):
54
+ """Android-compatible floating-point center of the match rectangle."""
55
+ left, top, right, bottom = self.rect
56
+ return ((left + right) / 2, (top + bottom) / 2)
57
+
58
+ def to_dict(self):
59
+ """将匹配结果转换为 JSON 兼容的字典。
60
+
61
+ 在 dataclass 默认字段基础上增加 x、y 顶层字段,方便前端 JS 直接调用。
62
+
63
+ Returns:
64
+ dict: 包含所有属性及额外 x、y 字段的字典。
65
+ """
66
+ d = asdict(self)
67
+ d.update({
68
+ "result": self.result,
69
+ "x": self.x,
70
+ "y": self.y,
71
+ "center_x": self.x,
72
+ "center_y": self.y,
73
+ })
74
+ return d
75
+
76
+ def to_android_dict(self):
77
+ """Return only the public fields emitted by Android FindImages."""
78
+ return {
79
+ "result": self.result,
80
+ "rectangle": self.rectangle,
81
+ "rect": self.rect,
82
+ "center_x": self.x,
83
+ "center_y": self.y,
84
+ "confidence": self.confidence,
85
+ "index": self.index,
86
+ }
87
+
88
+ def __getitem__(self, key):
89
+ """支持字典式键访问匹配结果属性。
90
+
91
+ Args:
92
+ key: 属性键名,支持 'result'(返回 center)、'rectangle'、'rect'、'confidence' 等。
93
+
94
+ Returns:
95
+ 对应键名的属性值。
96
+ """
97
+ if key == 'result': return self.result
98
+ if key == 'rectangle': return self.rectangle
99
+ if key == 'rect': return self.rect
100
+ if key == 'confidence': return self.confidence
101
+ return getattr(self, key)
102
+
103
+ def __str__(self):
104
+ return repr(self.to_android_dict())
105
+
106
+ def __repr__(self):
107
+ return repr(self.to_android_dict())
108
+
109
+
110
+ class AsCv:
111
+ """计算机视觉核心工具类。
112
+
113
+ 提供图像格式转换、模板匹配、多尺度匹配、特征匹配(AKAZE)等功能。
114
+ 所有方法均为静态方法或类方法,无需实例化即可使用。
115
+ """
116
+
117
+ @staticmethod
118
+ def to_numpy(im, preserve_alpha=False):
119
+ """将各种格式的图像统一转换为 NumPy 数组(OpenCV BGR 格式)。
120
+
121
+ 支持文件路径(含中文路径)、PIL Image 对象、NumPy 数组等输入。
122
+
123
+ Args:
124
+ im: 输入图像,可以是文件路径字符串、PIL.Image 对象、NumPy 数组或 None。
125
+
126
+ Returns:
127
+ numpy.ndarray: BGR 格式的图像数组,输入为 None 时返回 None。
128
+
129
+ Raises:
130
+ FileNotFoundError: 当文件路径无效或图像解析失败时抛出。
131
+ """
132
+ if im is None:
133
+ return None
134
+
135
+ # 1. 处理文件路径 (支持中文)
136
+ if isinstance(im, os.PathLike):
137
+ im = os.fspath(im)
138
+ if isinstance(im, str):
139
+ im_data = np.fromfile(im, dtype=np.uint8)
140
+ flag = cv2.IMREAD_UNCHANGED if preserve_alpha else cv2.IMREAD_COLOR
141
+ im = cv2.imdecode(im_data, flag)
142
+ if im is None:
143
+ raise FileNotFoundError(f"图像解析失败,请检查路径或格式: {im}")
144
+ return im
145
+
146
+ # 2. 处理 PIL Image 对象
147
+ if isinstance(im, Image.Image):
148
+ # PIL 是 RGB,OpenCV 是 BGR
149
+ # 使用 np.ascontiguousarray 确保内存连续,提升后续 mask 运算速度
150
+ if preserve_alpha and im.mode in {"RGBA", "LA"}:
151
+ return cv2.cvtColor(np.array(im.convert("RGBA")), cv2.COLOR_RGBA2BGRA)
152
+ return cv2.cvtColor(np.array(im.convert("RGB")), cv2.COLOR_RGB2BGR)
153
+
154
+ # 3. 如果已经是 NumPy 数组,直接返回 (确保是 uint8)
155
+ if isinstance(im, np.ndarray):
156
+ return im
157
+
158
+ return np.array(im, dtype=np.uint8)
159
+
160
+ @classmethod
161
+ def find_all_template(cls, im_source, im_search, threshold=0.8, maxcnt=0, rgb=False) -> List[MatchResult]:
162
+ """基础模板匹配,在源图像中查找所有与模板匹配的位置。
163
+
164
+ 使用归一化相关系数法(TM_CCOEFF_NORMED)进行匹配。
165
+ 支持灰度匹配和 RGB 分通道加权匹配两种模式。
166
+
167
+ Args:
168
+ im_source: 源图像(被搜索的大图),支持路径、PIL、NumPy 格式。
169
+ im_search: 模板图像(要查找的小图),支持路径、PIL、NumPy 格式。
170
+ threshold: 匹配置信度阈值,范围 0.0 ~ 1.0,默认为 0.8。
171
+ maxcnt: 最大返回结果数,0 表示不限制。1 时使用快速单结果模式。
172
+ rgb: 是否启用 RGB 分通道加权匹配模式,默认为 False(灰度匹配)。
173
+
174
+ Returns:
175
+ List[MatchResult]: 匹配结果列表,按置信度从高到低排列。
176
+ """
177
+ im_source = cls.to_numpy(im_source)
178
+ im_search = cls.to_numpy(im_search, preserve_alpha=True)
179
+ if im_source is None or im_search is None:
180
+ return []
181
+ if im_search.shape[0] > im_source.shape[0] or im_search.shape[1] > im_source.shape[1]:
182
+ return []
183
+ alpha = im_search[:, :, 3] if im_search.ndim == 3 and im_search.shape[2] == 4 else None
184
+ if alpha is not None:
185
+ im_search = im_search[:, :, :3]
186
+ w, h = im_search.shape[1], im_search.shape[0]
187
+ search_gray = cv2.cvtColor(im_search, cv2.COLOR_BGR2GRAY)
188
+ source_gray = cv2.cvtColor(im_source, cv2.COLOR_BGR2GRAY)
189
+ low_variance = float(np.std(search_gray)) < 1.0
190
+ method = cv2.TM_CCORR_NORMED if alpha is not None else (
191
+ cv2.TM_SQDIFF_NORMED if low_variance else cv2.TM_CCOEFF_NORMED
192
+ )
193
+
194
+ def normalize_result(value):
195
+ return 1.0 - value if low_variance and alpha is None else value
196
+
197
+ mask = alpha if alpha is not None else None
198
+
199
+ if rgb and mask is not None:
200
+ res = normalize_result(cv2.matchTemplate(
201
+ im_source, im_search, method, mask=mask
202
+ ))
203
+ elif rgb:
204
+ s_bgr, i_bgr = cv2.split(im_search), cv2.split(im_source)
205
+ res = (normalize_result(cv2.matchTemplate(i_bgr[0], s_bgr[0], method, mask=mask)) * 0.3 +
206
+ normalize_result(cv2.matchTemplate(i_bgr[1], s_bgr[1], method, mask=mask)) * 0.3 +
207
+ normalize_result(cv2.matchTemplate(i_bgr[2], s_bgr[2], method, mask=mask)) * 0.4)
208
+ else:
209
+ res = normalize_result(cv2.matchTemplate(source_gray, search_gray, method, mask=mask))
210
+
211
+ res = np.nan_to_num(res, nan=-1.0, posinf=-1.0, neginf=-1.0)
212
+
213
+ results = []
214
+
215
+ # 封装结果辅助函数
216
+ def wrap(val, loc, mth):
217
+ l, t = int(loc[0]), int(loc[1])
218
+ r, b = l + w, t + h
219
+ return MatchResult(
220
+ center=(l + w // 2, t + h // 2),
221
+ rectangle=[(l, t), (l, b), (r, t), (r, b)],
222
+ rect=[l, t, r, b], # 注入 LTRB
223
+ confidence=float(val),
224
+ method=mth
225
+ )
226
+
227
+ if maxcnt == 1:
228
+ _, max_val, _, max_loc = cv2.minMaxLoc(res)
229
+ return [wrap(max_val, max_loc, "template_fast")] if max_val >= threshold else []
230
+
231
+ while True:
232
+ _, max_val, _, max_loc = cv2.minMaxLoc(res)
233
+ if max_val < threshold:
234
+ break
235
+
236
+ results.append(wrap(max_val, max_loc, "template_multi"))
237
+ if 0 < maxcnt <= len(results):
238
+ break
239
+
240
+ margin_w, margin_h = max(1, w // 2), max(1, h // 2)
241
+ t = max(0, max_loc[1] - margin_h)
242
+ b = min(res.shape[0], max_loc[1] + margin_h + 1)
243
+ l = max(0, max_loc[0] - margin_w)
244
+ r = min(res.shape[1], max_loc[0] + margin_w + 1)
245
+ res[t:b, l:r] = -1.0
246
+
247
+ return results
248
+
249
+ @classmethod
250
+ def find_all_multi_scale(cls, im_source, im_search, threshold=0.8, maxcnt=0, scale_range=(0.5, 1.5),
251
+ steps=10, rgb=False) -> List[MatchResult]:
252
+ """多尺度模板匹配,在不同缩放比例下搜索模板图像。
253
+
254
+ 对模板图像进行多个比例的缩放后分别进行模板匹配,
255
+ 最终通过简易非极大值抑制去除重叠结果。
256
+
257
+ Args:
258
+ im_source: 源图像(被搜索的大图)。
259
+ im_search: 模板图像(要查找的小图)。
260
+ threshold: 匹配置信度阈值,默认为 0.8。
261
+ maxcnt: 最大返回结果数,0 表示不限制。
262
+ scale_range: 缩放比例范围,格式为 (最小比例, 最大比例),默认为 (0.5, 1.5)。
263
+ steps: 在缩放范围内的采样步数,默认为 10。
264
+
265
+ Returns:
266
+ List[MatchResult]: 去重后的匹配结果列表,按置信度从高到低排列。
267
+ """
268
+ im_source = cls.to_numpy(im_source)
269
+ im_search = cls.to_numpy(im_search, preserve_alpha=True)
270
+ all_candidates = []
271
+
272
+ for scale in np.linspace(scale_range[0], scale_range[1], steps):
273
+ tw, th = int(im_search.shape[1] * scale), int(im_search.shape[0] * scale)
274
+ if tw > im_source.shape[1] or th > im_source.shape[0] or tw < 10 or th < 10: continue
275
+
276
+ resized_sch = cv2.resize(im_search, (tw, th), interpolation=cv2.INTER_AREA)
277
+ step_res = cls.find_all_template(
278
+ im_source, resized_sch, threshold=threshold, maxcnt=maxcnt, rgb=rgb
279
+ )
280
+ for r in step_res:
281
+ r.scale = round(scale, 2)
282
+ r.method = "multi_scale"
283
+ all_candidates.append(r)
284
+
285
+ if not all_candidates: return []
286
+ # 按置信度排序
287
+ all_candidates.sort(key=lambda x: x.confidence, reverse=True)
288
+
289
+ # 非极大值抑制 (简易版):过滤重叠过近的结果
290
+ final = []
291
+ for cand in all_candidates:
292
+ if not any(np.linalg.norm(np.array(cand.center) - np.array(f.center)) < (min(im_search.shape[:2]) * 0.3)
293
+ for f in final):
294
+ final.append(cand)
295
+ if 0 < maxcnt <= len(final): break
296
+ return final
297
+
298
+ @classmethod
299
+ def find_akaze(cls, im_source, im_search, threshold=0.5, min_match=10) -> List[MatchResult]:
300
+ """基于 AKAZE 算法的特征点匹配。
301
+
302
+ 使用 AKAZE 特征检测器提取特征点,通过暴力匹配和 RANSAC 单应性估计
303
+ 定位模板在源图像中的位置。适用于视角变化、缩放等场景。
304
+
305
+ Args:
306
+ im_source: 源图像(被搜索的大图)。
307
+ im_search: 模板图像(要查找的小图)。
308
+ threshold: 内点比例阈值,默认为 0.5。
309
+ min_match: 最少需要的特征匹配点数,默认为 10。
310
+
311
+ Returns:
312
+ List[MatchResult]: 匹配结果列表,最多返回一个结果。
313
+ """
314
+ im_source, im_search = cls.to_numpy(im_source), cls.to_numpy(im_search)
315
+ akaze = cv2.AKAZE_create()
316
+ kp_sch, des_sch = akaze.detectAndCompute(im_search, None)
317
+ kp_src, des_src = akaze.detectAndCompute(im_source, None)
318
+
319
+ if des_sch is None or des_src is None or len(kp_sch) < min_match: return []
320
+
321
+ matches = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True).match(des_sch, des_src)
322
+ if len(matches) < min_match: return []
323
+
324
+ src_pts = np.float32([kp_sch[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
325
+ dst_pts = np.float32([kp_src[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)
326
+ M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
327
+
328
+ if M is not None:
329
+ conf = mask.sum() / len(matches)
330
+ if conf >= threshold:
331
+ h, w = im_search.shape[:2]
332
+ pts = np.float32([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2)
333
+ dst = cv2.perspectiveTransform(pts, M)
334
+
335
+ # 1. 原始多边形坐标
336
+ poly_pts = [tuple(npt[0].astype(int)) for npt in dst]
337
+
338
+ # 2. 计算外接矩形 LTRB (rect)
339
+ all_pts = dst.reshape(-1, 2)
340
+ l, t = int(all_pts[:, 0].min()), int(all_pts[:, 1].min())
341
+ r, b = int(all_pts[:, 0].max()), int(all_pts[:, 1].max())
342
+
343
+ rect_ltrb = [l, t, r, b]
344
+ center = ((l + r) // 2, (t + b) // 2)
345
+
346
+ return [MatchResult(center, poly_pts, rect_ltrb, round(float(conf), 3), "akaze")]
347
+ return []
348
+
349
+ @classmethod
350
+ def find(cls, im_source, im_search, threshold=0.8, maxcnt=1, use_scale=True,
351
+ rgb=False, scale_range=None) -> List[MatchResult]:
352
+ """综合图像查找入口。
353
+
354
+ 依次尝试模板匹配、多尺度匹配、AKAZE 特征匹配,直到找到足够数量的结果。
355
+
356
+ Args:
357
+ im_source: 源图像(被搜索的大图)。
358
+ im_search: 模板图像(要查找的小图)。
359
+ threshold: 匹配置信度阈值,默认为 0.8。
360
+ maxcnt: 最大返回结果数,默认为 1。0 表示不限制。
361
+ use_scale: 模板匹配结果不足时是否启用多尺度匹配,默认为 True。
362
+
363
+ Returns:
364
+ List[MatchResult]: 匹配结果列表。
365
+ """
366
+ res = cls.find_all_template(
367
+ im_source, im_search, threshold=threshold, maxcnt=maxcnt, rgb=rgb
368
+ )
369
+
370
+ # 如果模板匹配没找够,尝试多尺度
371
+ if (len(res) < maxcnt or maxcnt == 0) and use_scale:
372
+ needed = maxcnt - len(res) if maxcnt > 0 else 0
373
+ res.extend(cls.find_all_multi_scale(
374
+ im_source, im_search, threshold=threshold, maxcnt=needed,
375
+ scale_range=tuple(scale_range or (0.5, 1.5)), rgb=rgb,
376
+ ))
377
+
378
+ # 如果还是没找到,尝试特征匹配
379
+ if not res:
380
+ res = cls.find_akaze(im_source, im_search)
381
+
382
+ return res[:maxcnt] if maxcnt > 0 else res
@@ -0,0 +1,128 @@
1
+ import math
2
+ import random
3
+
4
+ import cv2
5
+
6
+ from .color_algorithms import _crop, _source_image
7
+
8
+
9
+ class Canny:
10
+ """边缘检测、轮廓过滤与基础形状识别。"""
11
+
12
+ RETR_TREE = cv2.RETR_TREE
13
+ RETR_LIST = cv2.RETR_LIST
14
+ RETR_EXTERNAL = cv2.RETR_EXTERNAL
15
+
16
+ DRAW_NONE = 0
17
+ DRAW_ALL = 1
18
+ DRAW_RECT = 2
19
+ DRAW_RECT_FILL = 3
20
+
21
+ @staticmethod
22
+ def _in_range(value, value_range, maximum):
23
+ if not value_range:
24
+ return True
25
+ low = value_range[0] if len(value_range) > 0 else 0
26
+ high = value_range[1] if len(value_range) > 1 and value_range[1] > 0 else maximum
27
+ return low <= value <= high
28
+
29
+ @staticmethod
30
+ def _shape(vertices, width, height, area, perimeter):
31
+ if vertices == 3:
32
+ return "triangle"
33
+ if vertices == 4:
34
+ ratio = float(width) / height if height else 0
35
+ return "square" if 0.85 <= ratio <= 1.15 else "rectangle"
36
+ if vertices == 5:
37
+ return "pentagon"
38
+ if vertices == 6:
39
+ return "hexagon"
40
+ if vertices > 6:
41
+ circularity = 4 * math.pi * area / (perimeter * perimeter) if perimeter else 0
42
+ return "circle" if circularity > 0.7 else "polygon"
43
+ return ""
44
+
45
+ @staticmethod
46
+ def detect(low_threshold: int = 50, high_threshold: int = 150, image=None, rect=None,
47
+ algo: str = "canny", blur: int = 3, mode: int = RETR_EXTERNAL,
48
+ draw: int = DRAW_NONE, area_range=None, width_range=None, height_range=None,
49
+ left_range=None, top_range=None, approx: float = 0.02,
50
+ detect_shape: bool = True, desc: bool = False, sobel_ksize: int = 3,
51
+ sobel_dir: str = "xy", block_size: int = 11, c_val: int = 2,
52
+ return_image: bool = False):
53
+ source, offset_x, offset_y = _crop(_source_image(image), rect)
54
+ output = source.copy()
55
+ image_height, image_width = source.shape[:2]
56
+ gray = cv2.cvtColor(source, cv2.COLOR_BGR2GRAY)
57
+ if blur > 0:
58
+ kernel = int(blur)
59
+ kernel = kernel if kernel % 2 else kernel + 1
60
+ gray = cv2.GaussianBlur(gray, (kernel, kernel), 0)
61
+
62
+ algorithm = str(algo).lower()
63
+ if algorithm == "sobel":
64
+ kernel = max(1, int(sobel_ksize))
65
+ kernel = kernel if kernel % 2 else kernel + 1
66
+ if sobel_dir == "x":
67
+ edges = cv2.convertScaleAbs(cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=kernel))
68
+ elif sobel_dir == "y":
69
+ edges = cv2.convertScaleAbs(cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=kernel))
70
+ else:
71
+ horizontal = cv2.convertScaleAbs(cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=kernel))
72
+ vertical = cv2.convertScaleAbs(cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=kernel))
73
+ edges = cv2.addWeighted(horizontal, 0.5, vertical, 0.5, 0)
74
+ _, edges = cv2.threshold(edges, 50, 255, cv2.THRESH_BINARY)
75
+ elif algorithm == "laplacian":
76
+ edges = cv2.convertScaleAbs(cv2.Laplacian(gray, cv2.CV_64F))
77
+ _, edges = cv2.threshold(edges, 30, 255, cv2.THRESH_BINARY)
78
+ elif algorithm == "adaptive":
79
+ block = max(3, int(block_size))
80
+ block = block if block % 2 else block + 1
81
+ edges = cv2.adaptiveThreshold(
82
+ gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, block, int(c_val)
83
+ )
84
+ else:
85
+ edges = cv2.Canny(gray, int(low_threshold), int(high_threshold))
86
+
87
+ contours, _ = cv2.findContours(edges, int(mode), cv2.CHAIN_APPROX_SIMPLE)
88
+ results = []
89
+ for contour in contours:
90
+ area = cv2.contourArea(contour)
91
+ perimeter = cv2.arcLength(contour, True)
92
+ left, top, width, height = cv2.boundingRect(contour)
93
+ if not Canny._in_range(area, area_range or [1, 0], image_width * image_height):
94
+ continue
95
+ if not Canny._in_range(width, width_range, image_width):
96
+ continue
97
+ if not Canny._in_range(height, height_range, image_height):
98
+ continue
99
+ if not Canny._in_range(left, left_range, image_width):
100
+ continue
101
+ if not Canny._in_range(top, top_range, image_height):
102
+ continue
103
+
104
+ approximated = cv2.approxPolyDP(contour, max(0.0, float(approx)) * perimeter, True) if perimeter else contour
105
+ vertices = len(approximated)
106
+ shape = Canny._shape(vertices, width, height, area, perimeter) if detect_shape else ""
107
+ moments = cv2.moments(contour)
108
+ center_x = int(moments["m10"] / moments["m00"]) if moments["m00"] else left + width // 2
109
+ center_y = int(moments["m01"] / moments["m00"]) if moments["m00"] else top + height // 2
110
+ color = tuple(random.randint(70, 240) for _ in range(3))
111
+ if draw == Canny.DRAW_ALL:
112
+ cv2.drawContours(output, [contour], -1, color, 2)
113
+ elif draw == Canny.DRAW_RECT:
114
+ cv2.rectangle(output, (left, top), (left + width, top + height), color, 2)
115
+ elif draw == Canny.DRAW_RECT_FILL:
116
+ cv2.rectangle(output, (left, top), (left + width, top + height), (255, 255, 255), -1)
117
+ if desc:
118
+ label = f"{shape} {width}x{height}" if shape else f"{width}x{height}"
119
+ cv2.putText(output, label, (left, max(12, top - 4)), cv2.FONT_HERSHEY_SIMPLEX, 0.4, color, 1)
120
+ results.append({
121
+ "x": left + offset_x, "y": top + offset_y, "w": width, "h": height,
122
+ "area": int(area), "perimeter": round(float(perimeter), 1),
123
+ "cx": center_x + offset_x, "cy": center_y + offset_y,
124
+ "vertices": vertices, "shape": shape,
125
+ "aspect": round(float(width) / height, 2) if height else 0,
126
+ })
127
+ results.sort(key=lambda item: (item["y"], item["x"]))
128
+ return (results, output, edges) if return_image else results
@@ -0,0 +1,72 @@
1
+ import cv2
2
+ import numpy as np
3
+
4
+ from .color_algorithms import _crop, _source_image
5
+
6
+
7
+ class CodeScanner:
8
+ """二维码与常见一维/二维条码识别。"""
9
+
10
+ @staticmethod
11
+ def _rect(points, offset_x: int, offset_y: int):
12
+ array = np.asarray(points, dtype=np.float32).reshape(-1, 2)
13
+ left, top = np.floor(array.min(axis=0)).astype(int)
14
+ right, bottom = np.ceil(array.max(axis=0)).astype(int)
15
+ return [left + offset_x, top + offset_y, right + offset_x, bottom + offset_y]
16
+
17
+ @staticmethod
18
+ def scan(rect=None, bitmap=None, image=None):
19
+ source, offset_x, offset_y = _crop(_source_image(image if image is not None else bitmap), rect)
20
+ results = []
21
+
22
+ try:
23
+ import zxingcpp
24
+
25
+ for item in zxingcpp.read_barcodes(cv2.cvtColor(source, cv2.COLOR_BGR2RGB)):
26
+ position = item.position
27
+ points = [position.top_left, position.top_right, position.bottom_right, position.bottom_left]
28
+ polygon = [[point.x, point.y] for point in points]
29
+ results.append({
30
+ "format": str(item.format).split(".")[-1], "type": "barcode",
31
+ "value": str(item.text), "rect": CodeScanner._rect(polygon, offset_x, offset_y),
32
+ })
33
+ except ImportError:
34
+ pass
35
+
36
+ if not results and hasattr(cv2, "barcode_BarcodeDetector"):
37
+ detector = cv2.barcode_BarcodeDetector()
38
+ ok, values, types, points = detector.detectAndDecodeWithType(source)
39
+ if ok and points is not None:
40
+ for value, code_type, polygon in zip(values, types, points):
41
+ if value:
42
+ results.append({
43
+ "format": str(code_type or "BARCODE"), "type": "barcode",
44
+ "value": str(value), "rect": CodeScanner._rect(polygon, offset_x, offset_y),
45
+ })
46
+
47
+ qr = cv2.QRCodeDetector()
48
+ ok, values, points, _ = qr.detectAndDecodeMulti(source)
49
+ if ok and points is not None:
50
+ for value, polygon in zip(values, points):
51
+ if value:
52
+ results.append({
53
+ "format": "QR_CODE", "type": "qrcode", "value": str(value),
54
+ "rect": CodeScanner._rect(polygon, offset_x, offset_y),
55
+ })
56
+ else:
57
+ value, points, _ = qr.detectAndDecode(source)
58
+ if value and points is not None:
59
+ results.append({
60
+ "format": "QR_CODE", "type": "qrcode", "value": str(value),
61
+ "rect": CodeScanner._rect(points, offset_x, offset_y),
62
+ })
63
+
64
+ unique = []
65
+ seen = set()
66
+ for item in results:
67
+ key = (item["value"], tuple(item["rect"]))
68
+ if key not in seen:
69
+ seen.add(key)
70
+ unique.append(item)
71
+ unique.sort(key=lambda item: (item["rect"][1], item["rect"][0]))
72
+ return unique