kotonebot 0.1.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.
Files changed (70) hide show
  1. kotonebot/__init__.py +40 -0
  2. kotonebot/backend/__init__.py +0 -0
  3. kotonebot/backend/bot.py +302 -0
  4. kotonebot/backend/color.py +525 -0
  5. kotonebot/backend/context/__init__.py +3 -0
  6. kotonebot/backend/context/context.py +1001 -0
  7. kotonebot/backend/context/task_action.py +176 -0
  8. kotonebot/backend/core.py +126 -0
  9. kotonebot/backend/debug/__init__.py +1 -0
  10. kotonebot/backend/debug/entry.py +89 -0
  11. kotonebot/backend/debug/mock.py +79 -0
  12. kotonebot/backend/debug/server.py +223 -0
  13. kotonebot/backend/debug/vars.py +346 -0
  14. kotonebot/backend/dispatch.py +228 -0
  15. kotonebot/backend/flow_controller.py +197 -0
  16. kotonebot/backend/image.py +748 -0
  17. kotonebot/backend/loop.py +277 -0
  18. kotonebot/backend/ocr.py +511 -0
  19. kotonebot/backend/preprocessor.py +103 -0
  20. kotonebot/client/__init__.py +10 -0
  21. kotonebot/client/device.py +500 -0
  22. kotonebot/client/fast_screenshot.py +378 -0
  23. kotonebot/client/host/__init__.py +12 -0
  24. kotonebot/client/host/adb_common.py +94 -0
  25. kotonebot/client/host/custom.py +114 -0
  26. kotonebot/client/host/leidian_host.py +202 -0
  27. kotonebot/client/host/mumu12_host.py +245 -0
  28. kotonebot/client/host/protocol.py +213 -0
  29. kotonebot/client/host/windows_common.py +55 -0
  30. kotonebot/client/implements/__init__.py +7 -0
  31. kotonebot/client/implements/adb.py +85 -0
  32. kotonebot/client/implements/adb_raw.py +159 -0
  33. kotonebot/client/implements/nemu_ipc/__init__.py +8 -0
  34. kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +280 -0
  35. kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -0
  36. kotonebot/client/implements/remote_windows.py +193 -0
  37. kotonebot/client/implements/uiautomator2.py +82 -0
  38. kotonebot/client/implements/windows.py +168 -0
  39. kotonebot/client/protocol.py +69 -0
  40. kotonebot/client/registration.py +24 -0
  41. kotonebot/config/__init__.py +1 -0
  42. kotonebot/config/base_config.py +96 -0
  43. kotonebot/config/manager.py +36 -0
  44. kotonebot/errors.py +72 -0
  45. kotonebot/interop/win/__init__.py +0 -0
  46. kotonebot/interop/win/message_box.py +314 -0
  47. kotonebot/interop/win/reg.py +37 -0
  48. kotonebot/interop/win/shortcut.py +43 -0
  49. kotonebot/interop/win/task_dialog.py +469 -0
  50. kotonebot/logging/__init__.py +2 -0
  51. kotonebot/logging/log.py +18 -0
  52. kotonebot/primitives/__init__.py +17 -0
  53. kotonebot/primitives/geometry.py +290 -0
  54. kotonebot/primitives/visual.py +63 -0
  55. kotonebot/tools/__init__.py +0 -0
  56. kotonebot/tools/mirror.py +354 -0
  57. kotonebot/ui/__init__.py +0 -0
  58. kotonebot/ui/file_host/sensio.py +36 -0
  59. kotonebot/ui/file_host/tmp_send.py +54 -0
  60. kotonebot/ui/pushkit/__init__.py +3 -0
  61. kotonebot/ui/pushkit/image_host.py +87 -0
  62. kotonebot/ui/pushkit/protocol.py +13 -0
  63. kotonebot/ui/pushkit/wxpusher.py +53 -0
  64. kotonebot/ui/user.py +144 -0
  65. kotonebot/util.py +409 -0
  66. kotonebot-0.1.0.dist-info/METADATA +204 -0
  67. kotonebot-0.1.0.dist-info/RECORD +70 -0
  68. kotonebot-0.1.0.dist-info/WHEEL +5 -0
  69. kotonebot-0.1.0.dist-info/licenses/LICENSE +674 -0
  70. kotonebot-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,525 @@
1
+ import colorsys
2
+ from typing import Literal
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+ import cv2
7
+ from cv2.typing import MatLike
8
+
9
+ from .core import unify_image
10
+ from ..primitives import RectTuple, Rect
11
+ from .debug import result as debug_result, debug, color as debug_color
12
+
13
+ RgbColorTuple = tuple[int, int, int]
14
+ RgbColorStr = str
15
+ RgbColor = RgbColorTuple | RgbColorStr
16
+ """颜色。三元组 `(r, g, b)` 或十六进制颜色字符串 `#RRGGBB`"""
17
+
18
+ HsvColor = tuple[int, int, int]
19
+ """
20
+ HSV颜色。三元组 `(h, s, v)`。
21
+ """
22
+
23
+ @dataclass
24
+ class FindColorPointResult:
25
+ position: tuple[int, int]
26
+ confidence: float
27
+ target_color: RgbColor
28
+
29
+ def hsv_web2cv(h: int, s: int, v: int) -> 'HsvColor':
30
+ """
31
+ 将 HSV 颜色从 Web 格式转换为 OpenCV 格式。
32
+
33
+ :param h: 色相,范围 [0, 360]
34
+ :param s: 饱和度,范围 [0, 100]
35
+ :param v: 亮度,范围 [0, 100]
36
+ :return: OpenCV 格式 HSV 颜色。三元组 `(h, s, v)`,范围分别为 (0-180, 0-255, 0-255)。
37
+ """
38
+ h = round(h / 2) # web 的色相范围是 0-360,转为 0-180
39
+ s = round(s / 100 * 255) # web 的饱和度范围是 0-100,转为 0-255
40
+ v = round(v / 100 * 255) # web 的亮度范围是 0-100,转为 0-255
41
+ return (h, s, v)
42
+
43
+ def hsv_cv2web(h: int, s: int, v: int) -> 'HsvColor':
44
+ """
45
+ 将 HSV 颜色从 OpenCV 格式转换为 Web 格式。
46
+
47
+ :param h: 色相,范围 [0, 180]
48
+ :param s: 饱和度,范围 [0, 255]
49
+ :param v: 亮度,范围 [0, 255]
50
+ :return: Web 格式 HSV 颜色。三元组 `(h, s, v)`,范围分别为 (0-360, 0-100, 0-100)。
51
+ """
52
+ h = round(h * 2) # opencv 的色相范围是 0-180,转为 0-360
53
+ s = round(s / 255 * 100) # opencv 的饱和度范围是 0-255,转为 0-100
54
+ v = round(v / 255 * 100) # opencv 的亮度范围是 0-255,转为 0-100
55
+ return (h, s, v)
56
+
57
+ def rgb_to_hsv(c: RgbColor) -> 'HsvColor':
58
+ """
59
+ 将 RGB 颜色转换为 HSV 颜色。
60
+
61
+ :param c: RGB 颜色。十六进制颜色字符串 `#RRGGBB` 或整数三元组 `(r, g, b)`。
62
+ :return: Web 格式 HSV 颜色。三元组 `(h, s, v)`,范围分别为 (0-360, 0-100, 0-100)。
63
+ """
64
+ c = _unify_color(c)
65
+ ret = colorsys.rgb_to_hsv(c[0] / 255, c[1] / 255, c[2] / 255)
66
+ return (round(ret[0] * 360), round(ret[1] * 100), round(ret[2] * 100))
67
+
68
+ def hsv_to_rgb(c: HsvColor) -> 'RgbColor':
69
+ """
70
+ 将 HSV 颜色转换为 RGB 颜色。
71
+
72
+ :param c: Web 格式 HSV 颜色。三元组 `(h, s, v)`,范围分别为 (0-360, 0-100, 0-100)。
73
+ :return: RGB 颜色。整数三元组 `(r, g, b)`。
74
+ """
75
+ ret = colorsys.hsv_to_rgb(c[0] / 360, c[1] / 100, c[2] / 100)
76
+ return (round(ret[0] * 255), round(ret[1] * 255), round(ret[2] * 255))
77
+
78
+ def _unify_color(color: RgbColor) -> RgbColorTuple:
79
+ if isinstance(color, str):
80
+ if not color.startswith('#'):
81
+ raise ValueError('Hex color string must start with #')
82
+ color = color[1:] # 去掉#
83
+ if len(color) != 6:
84
+ raise ValueError('Hex color string must be 6 digits')
85
+ r = int(color[0:2], 16)
86
+ g = int(color[2:4], 16)
87
+ b = int(color[4:6], 16)
88
+ return (r, g, b)
89
+ elif (
90
+ isinstance(color, tuple)
91
+ and len(color) == 3
92
+ and all(isinstance(c, int) for c in color)
93
+ and all(0 <= c <= 255 for c in color)
94
+ ):
95
+ return color
96
+ else:
97
+ raise ValueError('Invalid color format')
98
+
99
+ def in_range(color: RgbColor, range: tuple[HsvColor, HsvColor]) -> bool:
100
+ """
101
+ 判断颜色是否在范围内。
102
+
103
+ :param color: RGB 颜色。
104
+ :param range: Web HSV 颜色范围。
105
+ """
106
+ h, s, v = rgb_to_hsv(color)
107
+ h1, s1, v1 = range[0]
108
+ h2, s2, v2 = range[1]
109
+ return h1 <= h <= h2 and s1 <= s <= s2 and v1 <= v <= v2
110
+
111
+ def find(
112
+ image: MatLike | str,
113
+ color: RgbColor,
114
+ *,
115
+ rect: Rect | None = None,
116
+ threshold: float = 0.95,
117
+ method: Literal['rgb_dist'] = 'rgb_dist',
118
+ ) -> tuple[int, int] | None:
119
+ """
120
+ 在图像中查找指定颜色的点。
121
+
122
+ :param image:
123
+ 图像。可以是 MatLike 或图像文件路径。
124
+ 注意如果参数为 MatLike,则颜色格式必须为 BGR,而不是 RGB。
125
+ :param color: 颜色。可以是整数三元组 `(r, g, b)` 或十六进制颜色字符串 `#RRGGBB`。
126
+ :param rect: 查找范围。如果为 None,则在整个图像中查找。
127
+ :param threshold: 阈值,越大表示越相似,1 表示完全相似。默认为 0.95。
128
+ :param method: 比较算法。默认为 'rgb_dist',且目前也只有这个方法。
129
+
130
+ ## 比较算法
131
+ * rgb_dist:
132
+ 计算图片中每个点的颜色到目标颜色的欧氏距离,并以 442 为最大值归一化到 0-1 之间。
133
+ """
134
+ _rect = rect.xywh if rect else None
135
+ ret = None
136
+ ret_similarity = 0
137
+ found_color = None
138
+ color = _unify_color(color)
139
+ image = unify_image(image)
140
+ image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
141
+
142
+ # 将目标颜色转换为HSL
143
+ r, g, b = color
144
+ target_rgb = np.array([[[r, g, b]]], dtype=np.uint8)
145
+ target_hls = cv2.cvtColor(target_rgb, cv2.COLOR_RGB2HLS)[0,0]
146
+ target_h, target_l, target_s = target_hls
147
+
148
+ # 将图像转换为HSL
149
+ image_hls = cv2.cvtColor(image, cv2.COLOR_BGR2HLS).astype(np.float32)
150
+
151
+ # 计算HSL空间中的距离
152
+ # H通道需要特殊处理,因为它是环形的(0和180是相邻的)
153
+ h_diff = np.minimum(
154
+ np.abs(image_hls[:,:,0] - target_h),
155
+ 180 - np.abs(image_hls[:,:,0] - target_h)
156
+ )
157
+ l_diff = np.abs(image_hls[:,:,1] - target_l)
158
+ s_diff = np.abs(image_hls[:,:,2] - target_s)
159
+
160
+ # 归一化距离(H:0-180, L:0-255, S:0-255)
161
+ h_diff = h_diff / 90 # 最大差值180/2
162
+ l_diff = l_diff / 255
163
+ s_diff = s_diff / 255
164
+
165
+ # 计算加权距离
166
+ dist = np.sqrt((h_diff * 2)**2 + l_diff**2 + s_diff**2) / np.sqrt(6)
167
+
168
+ # 寻找结果
169
+ matches: np.ndarray = dist <= (1 - threshold)
170
+ # 只在rect范围内搜索
171
+ if _rect is not None:
172
+ x, y, w, h = _rect
173
+ search_area = matches[y:y+h, x:x+w]
174
+ if search_area.any():
175
+ # 在裁剪区域中找到最小距离的点
176
+ local_dist = dist[y:y+h, x:x+w]
177
+ local_dist[~search_area] = float('inf')
178
+ min_y, min_x = np.unravel_index(np.argmin(local_dist), local_dist.shape)
179
+ # 转换回原图坐标
180
+ ret = (int(x + min_x), int(y + min_y))
181
+ ret_similarity = 1 - local_dist[min_y, min_x]
182
+ found_color = tuple(image_rgb[y+min_y, x+min_x])
183
+ # 在全图中找到最小距离的点
184
+ else:
185
+ if matches.any():
186
+ dist[~matches] = float('inf')
187
+ min_y, min_x = np.unravel_index(np.argmin(dist), dist.shape)
188
+ ret = (int(min_x), int(min_y))
189
+ ret_similarity = 1 - dist[min_y, min_x]
190
+ found_color = tuple(image_rgb[min_y, min_x])
191
+ # 调试输出
192
+ if debug.enabled:
193
+ result_image = image.copy()
194
+ # 绘制结果点
195
+ if ret is not None:
196
+ x, y = ret
197
+ # 蓝色圈出结果点
198
+ cv2.rectangle(result_image,
199
+ (max(0, x-20), max(0, y-20)),
200
+ (min(result_image.shape[1], x+20), min(result_image.shape[0], y+20)),
201
+ (255, 0, 0), 2)
202
+ # 绘制搜索范围
203
+ if _rect is not None:
204
+ x, y, w, h = _rect
205
+ # 红色圈出rect
206
+ cv2.rectangle(result_image, (x, y), (x+w, y+h), (0, 0, 255), 2)
207
+ debug_result(
208
+ 'find_rgb',
209
+ [result_image, image],
210
+ f'target={debug_color(color)}\n'
211
+ f'rect={rect}\n'
212
+ f'result={ret}\n'
213
+ f'similarity={ret_similarity}\n'
214
+ f'found_color={debug_color(found_color)}\n'
215
+ '(Red rect for search area, blue rect for result area)'
216
+ )
217
+ return ret
218
+
219
+ def color_distance_map(
220
+ image: MatLike | str,
221
+ color: RgbColor,
222
+ *,
223
+ rect: RectTuple | None = None,
224
+ ) -> np.ndarray:
225
+ """
226
+ 计算图像中每个像素点到目标颜色的HSL距离,并返回归一化后的距离矩阵。
227
+
228
+ :param image: 图像。可以是 MatLike 或图像文件路径。
229
+ :param color: 目标颜色。可以是整数三元组 `(r, g, b)` 或十六进制颜色字符串 `#RRGGBB`。
230
+ :param rect: 计算范围。如果为 None,则在整个图像中计算。
231
+ :return: 归一化后的距离矩阵,范围 [0, 1],0 表示完全匹配,1 表示完全不匹配。
232
+ """
233
+ # 统一颜色格式
234
+ color = _unify_color(color)
235
+ # 统一图像格式
236
+ image = unify_image(image)
237
+
238
+ # # 如果指定了rect,裁剪图像
239
+ if rect is not None:
240
+ x, y, w, h = rect
241
+ image = image[y:y+h, x:x+w]
242
+
243
+ # 将图像转换为HLS格式
244
+ image_hls = cv2.cvtColor(image, cv2.COLOR_BGR2HLS).astype(np.float32)
245
+
246
+ # 将目标颜色转换为HSL
247
+ r, g, b = color
248
+ target_rgb = np.array([[[r, g, b]]], dtype=np.uint8)
249
+ target_hls = cv2.cvtColor(target_rgb, cv2.COLOR_RGB2HLS)[0,0]
250
+ target_h, target_l, target_s = target_hls
251
+
252
+ # 计算HSL空间中的距离
253
+ # H通道需要特殊处理,因为它是环形的(0和180是相邻的)
254
+ h_diff = np.minimum(
255
+ np.abs(image_hls[:,:,0] - target_h),
256
+ 180 - np.abs(image_hls[:,:,0] - target_h)
257
+ )
258
+ l_diff = np.abs(image_hls[:,:,1] - target_l)
259
+ s_diff = np.abs(image_hls[:,:,2] - target_s)
260
+
261
+ # 归一化距离(H:0-180, L:0-255, S:0-255)
262
+ h_diff = h_diff / 90 # 最大差值180/2
263
+ l_diff = l_diff / 255
264
+ s_diff = s_diff / 255
265
+
266
+ # 计算加权距离
267
+ dist = np.sqrt((h_diff * 2)**2 + l_diff**2 + s_diff**2) / np.sqrt(6)
268
+ return dist
269
+
270
+ def _rect_intersection(rect1: RectTuple, rect2: RectTuple) -> RectTuple | None:
271
+ """
272
+ 计算两个矩形的交集区域。
273
+
274
+ :param rect1: 第一个矩形 (x, y, w, h)
275
+ :param rect2: 第二个矩形 (x, y, w, h)
276
+ :return: 交集矩形 (x, y, w, h),如果没有交集则返回 None
277
+ """
278
+ x1 = max(rect1[0], rect2[0])
279
+ y1 = max(rect1[1], rect2[1])
280
+ x2 = min(rect1[0] + rect1[2], rect2[0] + rect2[2])
281
+ y2 = min(rect1[1] + rect1[3], rect2[1] + rect2[3])
282
+
283
+ if x1 >= x2 or y1 >= y2:
284
+ return None
285
+
286
+ return (x1, y1, x2 - x1, y2 - y1)
287
+
288
+ def find_all(
289
+ image: MatLike | str,
290
+ color: RgbColor,
291
+ *,
292
+ rect: Rect | None = None,
293
+ threshold: float = 0.95,
294
+ method: Literal['rgb_dist'] = 'rgb_dist',
295
+ filter_method: Literal['point', 'contour'] = 'contour',
296
+ max_results: int | None = None,
297
+ ) -> list[FindColorPointResult]:
298
+ """
299
+ 在图像中查找所有符合指定颜色的点。
300
+
301
+ :param image:
302
+ 图像。可以是 MatLike 或图像文件路径。
303
+ 注意如果参数为 MatLike,则颜色格式必须为 BGR,而不是 RGB。
304
+ :param color: 颜色。可以是整数三元组 `(r, g, b)` 或十六进制颜色字符串 `#RRGGBB`。
305
+ :param rect: 查找范围。如果为 None,则在整个图像中查找。
306
+ :param threshold: 阈值,越大表示越相似,1 表示完全相似。默认为 0.95。
307
+ :param method: 比较算法。默认为 'rgb_dist',且目前也只有这个方法。
308
+ :param filter_method: 查找方法。
309
+
310
+ * point:按点查找结果
311
+ * contour:按轮廓查找结果。也就是如果很多个符合要求的点连在一起,会被当做一个整体返回。
312
+ :param max_results: 最大返回结果数量。如果为 None,则返回所有结果。
313
+ :return: 结果列表。
314
+ """
315
+ _rect: RectTuple | None = rect.xywh if rect is not None else None
316
+ # 计算距离矩阵
317
+ dist = color_distance_map(image, color)
318
+ # 筛选满足要求的点,二值化
319
+ binary = np.where(dist <= (1 - threshold), 255, 0).astype(np.uint8)
320
+
321
+ def filter_by_point(binary: np.ndarray, target_color: RgbColor, dist: np.ndarray, rect: Rect | None = None, max_results: int | None = None) -> list[FindColorPointResult]:
322
+ results = []
323
+
324
+ if _rect is not None:
325
+ x, y, w, h = _rect
326
+ search_area = binary[y:y+h, x:x+w]
327
+ local_dist = dist[y:y+h, x:x+w]
328
+
329
+ # 获取所有匹配点的坐标
330
+ match_coords = np.where(search_area)
331
+ for local_y, local_x in zip(*match_coords):
332
+ confidence = 1 - local_dist[local_y, local_x]
333
+ global_x = x + local_x
334
+ global_y = y + local_y
335
+ results.append(FindColorPointResult(
336
+ position=(int(global_x), int(global_y)),
337
+ confidence=float(confidence),
338
+ target_color=target_color
339
+ ))
340
+ else:
341
+ # 获取所有匹配点的坐标
342
+ match_coords = np.where(binary)
343
+ for y, x in zip(*match_coords):
344
+ confidence = 1 - dist[y, x]
345
+ results.append(FindColorPointResult(
346
+ position=(int(x), int(y)),
347
+ confidence=float(confidence),
348
+ target_color=target_color
349
+ ))
350
+
351
+ # 按置信度排序
352
+ results.sort(key=lambda x: x.confidence, reverse=True)
353
+
354
+ # 限制结果数量
355
+ if max_results is not None:
356
+ results = results[:max_results]
357
+
358
+ return results
359
+
360
+ def filter_by_contour(binary: np.ndarray, target_color: RgbColor, dist: np.ndarray, rect: Rect | None = None, max_results: int | None = None) -> list[FindColorPointResult]:
361
+ # 查找轮廓
362
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
363
+
364
+ if not contours:
365
+ return []
366
+
367
+ results = []
368
+ for contour in contours:
369
+ # 获取轮廓的外界矩形
370
+ contour_rect = cv2.boundingRect(contour)
371
+ contour_rect = tuple(contour_rect)
372
+ assert len(contour_rect) == 4
373
+
374
+ # 如果指定了rect,计算轮廓外接矩形与rect的交集
375
+ if _rect is not None:
376
+ intersection = _rect_intersection(contour_rect, _rect)
377
+ if intersection is None:
378
+ continue
379
+
380
+ x1, y1, w, h = intersection
381
+ # 计算交集区域的中心点
382
+ cx = x1 + w // 2
383
+ cy = y1 + h // 2
384
+
385
+ # 创建掩码,只保留当前轮廓内的区域
386
+ mask = np.zeros_like(binary)
387
+ cv2.drawContours(mask, [contour], 0, (255,), -1)
388
+
389
+ # 只计算交集区域内的平均距离
390
+ mask[y1:y1+h, x1:x1+w] = mask[y1:y1+h, x1:x1+w] & binary[y1:y1+h, x1:x1+w]
391
+ avg_dist = np.mean(dist[y1:y1+h, x1:x1+w][mask[y1:y1+h, x1:x1+w] == 255])
392
+ else:
393
+ # 计算轮廓中心点
394
+ cx = contour_rect[0] + contour_rect[2] // 2
395
+ cy = contour_rect[1] + contour_rect[3] // 2
396
+
397
+ # 创建掩码,只保留当前轮廓内的区域
398
+ mask = np.zeros_like(binary)
399
+ cv2.drawContours(mask, [contour], 0, (255,), -1)
400
+
401
+ # 计算整个轮廓区域内的平均距离
402
+ avg_dist = np.mean(dist[mask == 255])
403
+
404
+ confidence = float(1 - avg_dist)
405
+
406
+ results.append(FindColorPointResult(
407
+ position=(cx, cy),
408
+ confidence=confidence,
409
+ target_color=target_color
410
+ ))
411
+
412
+ # 按置信度排序
413
+ results.sort(key=lambda x: x.confidence, reverse=True)
414
+
415
+ # 限制结果数量
416
+ if max_results is not None:
417
+ results = results[:max_results]
418
+
419
+ return results
420
+
421
+ # 根据filter_method选择过滤方法
422
+ if filter_method == 'point':
423
+ results = filter_by_point(binary, color, dist, rect, max_results)
424
+ else: # filter_method == 'contour'
425
+ results = filter_by_contour(binary, color, dist, rect, max_results)
426
+
427
+ # 调试输出
428
+ if debug.enabled:
429
+ result_image = unify_image(image).copy()
430
+ # 绘制所有结果点
431
+ for result in results:
432
+ x, y = result.position
433
+ cv2.rectangle(result_image,
434
+ (max(0, x-10), max(0, y-10)),
435
+ (min(result_image.shape[1], x+10), min(result_image.shape[0], y+10)),
436
+ (255, 0, 0), 1)
437
+ # 绘制搜索范围
438
+ if _rect is not None:
439
+ x, y, w, h = _rect
440
+ cv2.rectangle(result_image, (x, y), (x+w, y+h), (0, 0, 255), 2)
441
+
442
+ debug_result(
443
+ 'find_rgb_many',
444
+ [result_image, unify_image(image)],
445
+ f'target={debug_color(color)}\n'
446
+ f'rect={rect}\n'
447
+ f'found {len(results)} points\n'
448
+ f'threshold={threshold}\n'
449
+ f'filter_method={filter_method}\n'
450
+ '(Red rect for search area, blue rects for result areas)'
451
+ )
452
+
453
+ return results
454
+
455
+ # https://stackoverflow.com/questions/43111029/how-to-find-the-average-colour-of-an-image-in-python-with-opencv
456
+ def dominant_color(
457
+ image: MatLike | str,
458
+ count: int = 1,
459
+ *,
460
+ rect: Rect | None = None,
461
+ ) -> list[RgbColorStr]:
462
+ """
463
+ 提取图像的主色调。
464
+
465
+ :param image:
466
+ 图像。可以是 MatLike 或图像文件路径。
467
+ 如果是 MatLike,则颜色格式必须为 BGR。
468
+ :param count: 提取的颜色数量。默认为 1。
469
+ :param rect: 提取范围。如果为 None,则在整个图像中提取。
470
+ """
471
+ _rect: RectTuple | None = rect.xywh if rect is not None else None
472
+ # 载入/裁剪图像
473
+ img = unify_image(image)
474
+ if _rect is not None:
475
+ x, y, w, h = _rect
476
+ img = img[y:y+h, x:x+w]
477
+
478
+ pixels = np.float32(img.reshape(-1, 3))
479
+
480
+ n_colors = count
481
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, .1)
482
+ flags = cv2.KMEANS_RANDOM_CENTERS
483
+
484
+ _, labels, palette = cv2.kmeans(pixels, n_colors, None, criteria, 10, flags) # type: ignore
485
+ _, counts = np.unique(labels, return_counts=True)
486
+
487
+ # 将颜色按出现次数排序
488
+ indices = (-counts).argsort()
489
+ dominant = palette[indices]
490
+
491
+ # 转换为 RGB 格式并转为 16 进制颜色代码
492
+ result: list[RgbColorStr] = []
493
+ for i in range(min(n_colors, len(dominant))):
494
+ color = dominant[i]
495
+ # BGR -> RGB
496
+ rgb = tuple(map(int, color[::-1]))
497
+ hex_color = f'#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}'
498
+ result.append(hex_color)
499
+
500
+ if debug.enabled:
501
+ origin_image = unify_image(image)
502
+ result_image = origin_image.copy()
503
+ if _rect is not None:
504
+ x, y, w, h = _rect
505
+ cv2.rectangle(result_image, (x, y), (x + w, y + h), (0, 0, 255), 2)
506
+ debug_result(
507
+ 'color.dominant_color',
508
+ [result_image, origin_image],
509
+ f'arguments:\n \tcount={count}\n \trect={rect}\n'
510
+ f'result={", ".join(map(debug_color, result))}'
511
+ )
512
+
513
+ return result
514
+
515
+ if __name__ == '__main__':
516
+ img = cv2.imread('tests/images/ui/commu_fast_forward_enabled.png')
517
+ colors = dominant_color(img, 3)
518
+ print("主调颜色:")
519
+ for i, color in enumerate(colors, 1):
520
+ print(f"{i}. {color}")
521
+ # 创建一个纯色图像来展示颜色
522
+ color_block = np.full((100, 100, 3), _unify_color(color)[::-1], dtype=np.uint8)
523
+ cv2.imshow(f"Color {i}", color_block)
524
+ cv2.waitKey(0)
525
+ cv2.destroyAllWindows()
@@ -0,0 +1,3 @@
1
+ from .context import *
2
+ from .context import _c
3
+ from .task_action import task, action, task_registry, action_registry, current_callstack, Task, Action, tasks_from_id