kotonebot 0.1.0__py3-none-any.whl → 0.3.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 +1008 -1000
- kotonebot/backend/debug/vars.py +6 -1
- kotonebot/backend/image.py +778 -748
- kotonebot/backend/loop.py +283 -276
- kotonebot/backend/ocr.py +20 -2
- kotonebot/client/device.py +6 -3
- kotonebot/client/host/mumu12_host.py +157 -44
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +5 -0
- kotonebot-0.3.0.dist-info/METADATA +76 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/RECORD +13 -13
- kotonebot-0.1.0.dist-info/METADATA +0 -204
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {kotonebot-0.1.0.dist-info → kotonebot-0.3.0.dist-info}/top_level.txt +0 -0
kotonebot/backend/image.py
CHANGED
|
@@ -1,748 +1,778 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from logging import getLogger
|
|
3
|
-
from typing import NamedTuple, Protocol, Sequence, runtime_checkable
|
|
4
|
-
|
|
5
|
-
import cv2
|
|
6
|
-
import numpy as np
|
|
7
|
-
from cv2.typing import MatLike, Rect as CvRect
|
|
8
|
-
from skimage.metrics import structural_similarity
|
|
9
|
-
|
|
10
|
-
from .core import Image, unify_image
|
|
11
|
-
from .preprocessor import PreprocessorProtocol
|
|
12
|
-
from kotonebot.primitives import Point as KbPoint, Rect as KbRect, Size as KbSize
|
|
13
|
-
from .debug import result as debug_result, debug, img
|
|
14
|
-
|
|
15
|
-
logger = getLogger(__name__)
|
|
16
|
-
|
|
17
|
-
class TemplateNoMatchError(Exception):
|
|
18
|
-
"""模板未找到异常。"""
|
|
19
|
-
def __init__(self, image: MatLike | Image, template: MatLike | str | Image):
|
|
20
|
-
self.image = image
|
|
21
|
-
self.template = template
|
|
22
|
-
super().__init__(f"Template not found: {template}")
|
|
23
|
-
|
|
24
|
-
@runtime_checkable
|
|
25
|
-
class ResultProtocol(Protocol):
|
|
26
|
-
@property
|
|
27
|
-
def rect(self) -> KbRect:
|
|
28
|
-
"""结果区域。左上角坐标和宽高。"""
|
|
29
|
-
...
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class TemplateMatchResult(NamedTuple):
|
|
33
|
-
score: float
|
|
34
|
-
position: KbPoint
|
|
35
|
-
"""结果位置。左上角坐标。"""
|
|
36
|
-
size: KbSize
|
|
37
|
-
"""输入模板的大小。宽高。"""
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def rect(self) -> KbRect:
|
|
41
|
-
"""结果区域。"""
|
|
42
|
-
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
|
|
43
|
-
|
|
44
|
-
@property
|
|
45
|
-
def right_bottom(self) -> KbPoint:
|
|
46
|
-
"""结果右下角坐标。"""
|
|
47
|
-
return KbPoint(self.position[0] + self.size[0], self.position[1] + self.size[1])
|
|
48
|
-
|
|
49
|
-
class MultipleTemplateMatchResult(NamedTuple):
|
|
50
|
-
score: float
|
|
51
|
-
position: KbPoint
|
|
52
|
-
"""结果位置。左上角坐标。"""
|
|
53
|
-
size: KbSize
|
|
54
|
-
"""命中模板的大小。宽高。"""
|
|
55
|
-
index: int
|
|
56
|
-
"""命中模板在列表中的索引。"""
|
|
57
|
-
|
|
58
|
-
@property
|
|
59
|
-
def rect(self) -> KbRect:
|
|
60
|
-
"""结果区域。左上角坐标和宽高。"""
|
|
61
|
-
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
|
|
62
|
-
|
|
63
|
-
@property
|
|
64
|
-
def right_bottom(self) -> KbPoint:
|
|
65
|
-
"""结果右下角坐标。"""
|
|
66
|
-
return KbPoint(self.position[0] + self.size[0], self.position[1] + self.size[1])
|
|
67
|
-
|
|
68
|
-
@classmethod
|
|
69
|
-
def from_template_match_result(cls, result: TemplateMatchResult, index: int):
|
|
70
|
-
return cls(
|
|
71
|
-
score=result.score,
|
|
72
|
-
position=result.position,
|
|
73
|
-
size=result.size,
|
|
74
|
-
index=index
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
class CropResult(NamedTuple):
|
|
78
|
-
score: float
|
|
79
|
-
position: KbPoint
|
|
80
|
-
size: KbSize
|
|
81
|
-
image: MatLike
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def rect(self) -> KbRect:
|
|
85
|
-
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
|
|
86
|
-
|
|
87
|
-
def _draw_result(image: MatLike, matches: Sequence[ResultProtocol] | ResultProtocol | None) -> MatLike:
|
|
88
|
-
"""在图像上绘制匹配结果的矩形框。"""
|
|
89
|
-
if matches is None:
|
|
90
|
-
return image
|
|
91
|
-
if isinstance(matches, ResultProtocol):
|
|
92
|
-
matches = [matches]
|
|
93
|
-
result_image = image.copy()
|
|
94
|
-
for match in matches:
|
|
95
|
-
cv2.rectangle(result_image, match.rect.xywh, (0, 0, 255), 2)
|
|
96
|
-
return result_image
|
|
97
|
-
|
|
98
|
-
def _img2str(image: MatLike | str | Image | None) -> str:
|
|
99
|
-
if image is None:
|
|
100
|
-
return 'None'
|
|
101
|
-
if isinstance(image, str):
|
|
102
|
-
try:
|
|
103
|
-
return os.path.relpath(image)
|
|
104
|
-
except ValueError:
|
|
105
|
-
# ValueError: path is on mount 'C:', start on mount 'E:'
|
|
106
|
-
# 程序路径与资源路径不在同一个地方的情况
|
|
107
|
-
return image
|
|
108
|
-
elif isinstance(image, Image):
|
|
109
|
-
return f'<Image: {image.name} at {image.path}>'
|
|
110
|
-
else:
|
|
111
|
-
return '<opencv Mat>'
|
|
112
|
-
|
|
113
|
-
def _imgs2str(images: Sequence[MatLike | str | Image | None] | None) -> str:
|
|
114
|
-
if images is None:
|
|
115
|
-
return 'None'
|
|
116
|
-
return ', '.join([_img2str(image) for image in images])
|
|
117
|
-
|
|
118
|
-
def _result2str(result: TemplateMatchResult | MultipleTemplateMatchResult | None) -> str:
|
|
119
|
-
if result is None:
|
|
120
|
-
return 'None'
|
|
121
|
-
return f'{result.rect} {result.score}'
|
|
122
|
-
|
|
123
|
-
def _results2str(results: Sequence[TemplateMatchResult | MultipleTemplateMatchResult] | None) -> str:
|
|
124
|
-
if results is None:
|
|
125
|
-
return 'None'
|
|
126
|
-
return ', '.join([_result2str(result) for result in results])
|
|
127
|
-
|
|
128
|
-
# TODO: 应该把 template_match 和 find、wait、expect 等函数的公共参数提取出来
|
|
129
|
-
# TODO: 需要在调试结果中输出 preprocessors 处理后的图像
|
|
130
|
-
def template_match(
|
|
131
|
-
template: MatLike | str | Image,
|
|
132
|
-
image: MatLike | str | Image,
|
|
133
|
-
mask: MatLike | str | Image | None = None,
|
|
134
|
-
*,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
:param
|
|
151
|
-
:param
|
|
152
|
-
:param
|
|
153
|
-
:param
|
|
154
|
-
:param
|
|
155
|
-
:param
|
|
156
|
-
:param
|
|
157
|
-
:param
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
:
|
|
255
|
-
:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
roi[
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
:
|
|
323
|
-
|
|
324
|
-
:
|
|
325
|
-
:
|
|
326
|
-
:
|
|
327
|
-
:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
template:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
:
|
|
369
|
-
|
|
370
|
-
:
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
:
|
|
424
|
-
:
|
|
425
|
-
:
|
|
426
|
-
|
|
427
|
-
:
|
|
428
|
-
:
|
|
429
|
-
:
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
:
|
|
476
|
-
:
|
|
477
|
-
:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
image:
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
:
|
|
549
|
-
:
|
|
550
|
-
:
|
|
551
|
-
|
|
552
|
-
:
|
|
553
|
-
:
|
|
554
|
-
:
|
|
555
|
-
:
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1
|
+
import os
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from typing import NamedTuple, Protocol, Sequence, runtime_checkable
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
import numpy as np
|
|
7
|
+
from cv2.typing import MatLike, Rect as CvRect
|
|
8
|
+
from skimage.metrics import structural_similarity
|
|
9
|
+
|
|
10
|
+
from .core import Image, unify_image
|
|
11
|
+
from .preprocessor import PreprocessorProtocol
|
|
12
|
+
from kotonebot.primitives import Point as KbPoint, Rect as KbRect, Size as KbSize
|
|
13
|
+
from .debug import result as debug_result, debug, img
|
|
14
|
+
|
|
15
|
+
logger = getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class TemplateNoMatchError(Exception):
|
|
18
|
+
"""模板未找到异常。"""
|
|
19
|
+
def __init__(self, image: MatLike | Image, template: MatLike | str | Image):
|
|
20
|
+
self.image = image
|
|
21
|
+
self.template = template
|
|
22
|
+
super().__init__(f"Template not found: {template}")
|
|
23
|
+
|
|
24
|
+
@runtime_checkable
|
|
25
|
+
class ResultProtocol(Protocol):
|
|
26
|
+
@property
|
|
27
|
+
def rect(self) -> KbRect:
|
|
28
|
+
"""结果区域。左上角坐标和宽高。"""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TemplateMatchResult(NamedTuple):
|
|
33
|
+
score: float
|
|
34
|
+
position: KbPoint
|
|
35
|
+
"""结果位置。左上角坐标。"""
|
|
36
|
+
size: KbSize
|
|
37
|
+
"""输入模板的大小。宽高。"""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def rect(self) -> KbRect:
|
|
41
|
+
"""结果区域。"""
|
|
42
|
+
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def right_bottom(self) -> KbPoint:
|
|
46
|
+
"""结果右下角坐标。"""
|
|
47
|
+
return KbPoint(self.position[0] + self.size[0], self.position[1] + self.size[1])
|
|
48
|
+
|
|
49
|
+
class MultipleTemplateMatchResult(NamedTuple):
|
|
50
|
+
score: float
|
|
51
|
+
position: KbPoint
|
|
52
|
+
"""结果位置。左上角坐标。"""
|
|
53
|
+
size: KbSize
|
|
54
|
+
"""命中模板的大小。宽高。"""
|
|
55
|
+
index: int
|
|
56
|
+
"""命中模板在列表中的索引。"""
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def rect(self) -> KbRect:
|
|
60
|
+
"""结果区域。左上角坐标和宽高。"""
|
|
61
|
+
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def right_bottom(self) -> KbPoint:
|
|
65
|
+
"""结果右下角坐标。"""
|
|
66
|
+
return KbPoint(self.position[0] + self.size[0], self.position[1] + self.size[1])
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_template_match_result(cls, result: TemplateMatchResult, index: int):
|
|
70
|
+
return cls(
|
|
71
|
+
score=result.score,
|
|
72
|
+
position=result.position,
|
|
73
|
+
size=result.size,
|
|
74
|
+
index=index
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
class CropResult(NamedTuple):
|
|
78
|
+
score: float
|
|
79
|
+
position: KbPoint
|
|
80
|
+
size: KbSize
|
|
81
|
+
image: MatLike
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def rect(self) -> KbRect:
|
|
85
|
+
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
|
|
86
|
+
|
|
87
|
+
def _draw_result(image: MatLike, matches: Sequence[ResultProtocol] | ResultProtocol | None) -> MatLike:
|
|
88
|
+
"""在图像上绘制匹配结果的矩形框。"""
|
|
89
|
+
if matches is None:
|
|
90
|
+
return image
|
|
91
|
+
if isinstance(matches, ResultProtocol):
|
|
92
|
+
matches = [matches]
|
|
93
|
+
result_image = image.copy()
|
|
94
|
+
for match in matches:
|
|
95
|
+
cv2.rectangle(result_image, match.rect.xywh, (0, 0, 255), 2)
|
|
96
|
+
return result_image
|
|
97
|
+
|
|
98
|
+
def _img2str(image: MatLike | str | Image | None) -> str:
|
|
99
|
+
if image is None:
|
|
100
|
+
return 'None'
|
|
101
|
+
if isinstance(image, str):
|
|
102
|
+
try:
|
|
103
|
+
return os.path.relpath(image)
|
|
104
|
+
except ValueError:
|
|
105
|
+
# ValueError: path is on mount 'C:', start on mount 'E:'
|
|
106
|
+
# 程序路径与资源路径不在同一个地方的情况
|
|
107
|
+
return image
|
|
108
|
+
elif isinstance(image, Image):
|
|
109
|
+
return f'<Image: {image.name} at {image.path}>'
|
|
110
|
+
else:
|
|
111
|
+
return '<opencv Mat>'
|
|
112
|
+
|
|
113
|
+
def _imgs2str(images: Sequence[MatLike | str | Image | None] | None) -> str:
|
|
114
|
+
if images is None:
|
|
115
|
+
return 'None'
|
|
116
|
+
return ', '.join([_img2str(image) for image in images])
|
|
117
|
+
|
|
118
|
+
def _result2str(result: TemplateMatchResult | MultipleTemplateMatchResult | None) -> str:
|
|
119
|
+
if result is None:
|
|
120
|
+
return 'None'
|
|
121
|
+
return f'{result.rect} {result.score}'
|
|
122
|
+
|
|
123
|
+
def _results2str(results: Sequence[TemplateMatchResult | MultipleTemplateMatchResult] | None) -> str:
|
|
124
|
+
if results is None:
|
|
125
|
+
return 'None'
|
|
126
|
+
return ', '.join([_result2str(result) for result in results])
|
|
127
|
+
|
|
128
|
+
# TODO: 应该把 template_match 和 find、wait、expect 等函数的公共参数提取出来
|
|
129
|
+
# TODO: 需要在调试结果中输出 preprocessors 处理后的图像
|
|
130
|
+
def template_match(
|
|
131
|
+
template: MatLike | str | Image,
|
|
132
|
+
image: MatLike | str | Image,
|
|
133
|
+
mask: MatLike | str | Image | None = None,
|
|
134
|
+
*,
|
|
135
|
+
rect: KbRect | None = None,
|
|
136
|
+
transparent: bool = False,
|
|
137
|
+
threshold: float = 0.8,
|
|
138
|
+
max_results: int = 5,
|
|
139
|
+
remove_duplicate: bool = True,
|
|
140
|
+
colored: bool = False,
|
|
141
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
142
|
+
) -> list[TemplateMatchResult]:
|
|
143
|
+
"""
|
|
144
|
+
寻找模板在图像中的位置。
|
|
145
|
+
|
|
146
|
+
.. note::
|
|
147
|
+
`mask` 和 `transparent` 参数不能同时使用。
|
|
148
|
+
如果使用透明图像,所有透明像素必须为 100% 透明,不能包含半透明像素。
|
|
149
|
+
|
|
150
|
+
:param template: 模板图像,可以是图像路径或 cv2.Mat。
|
|
151
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
152
|
+
:param mask: 掩码图像,可以是图像路径或 cv2.Mat。
|
|
153
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
154
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
155
|
+
:param threshold: 阈值,默认为 0.8。
|
|
156
|
+
:param max_results: 最大结果数,默认为 1。
|
|
157
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
158
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
159
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
160
|
+
"""
|
|
161
|
+
# 统一参数
|
|
162
|
+
template = unify_image(template, transparent)
|
|
163
|
+
image = unify_image(image)
|
|
164
|
+
|
|
165
|
+
# 处理矩形区域
|
|
166
|
+
original_image = image
|
|
167
|
+
if rect is not None:
|
|
168
|
+
x, y, w, h = rect.xywh
|
|
169
|
+
image = image[y:y+h, x:x+w]
|
|
170
|
+
|
|
171
|
+
if transparent is True and mask is not None:
|
|
172
|
+
raise ValueError('mask and transparent cannot be used together')
|
|
173
|
+
if mask is not None:
|
|
174
|
+
mask = unify_image(mask)
|
|
175
|
+
mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)[1]
|
|
176
|
+
if transparent is True:
|
|
177
|
+
# https://stackoverflow.com/questions/57899997/how-to-create-mask-from-alpha-channel-in-opencv
|
|
178
|
+
# 从透明图像中提取 alpha 通道作为 mask
|
|
179
|
+
mask = cv2.threshold(template[:, :, 3], 0, 255, cv2.THRESH_BINARY)[1]
|
|
180
|
+
template = template[:, :, :3]
|
|
181
|
+
# 预处理
|
|
182
|
+
if preprocessors is not None:
|
|
183
|
+
for preprocessor in preprocessors:
|
|
184
|
+
image = preprocessor.process(image)
|
|
185
|
+
template = preprocessor.process(template)
|
|
186
|
+
if mask is not None:
|
|
187
|
+
mask = preprocessor.process(mask)
|
|
188
|
+
# 匹配模板
|
|
189
|
+
if mask is not None:
|
|
190
|
+
# https://stackoverflow.com/questions/35642497/python-opencv-cv2-matchtemplate-with-transparency
|
|
191
|
+
# 使用 Mask 时,必须使用 TM_CCORR_NORMED 方法
|
|
192
|
+
result = cv2.matchTemplate(image, template, cv2.TM_CCORR_NORMED, mask=mask)
|
|
193
|
+
else:
|
|
194
|
+
result = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
|
|
195
|
+
|
|
196
|
+
# ========== 整理结果 ==========
|
|
197
|
+
# 去重、排序、转换为 TemplateMatchResult
|
|
198
|
+
|
|
199
|
+
# 获取所有大于阈值的匹配结果并按分数排序
|
|
200
|
+
h, w = template.shape[:2]
|
|
201
|
+
matches = []
|
|
202
|
+
if remove_duplicate:
|
|
203
|
+
# 创建一个掩码来标记已匹配区域
|
|
204
|
+
used_mask = np.zeros((image.shape[0], image.shape[1]), np.uint8)
|
|
205
|
+
|
|
206
|
+
# 获取所有匹配点并按分数从高到低排序
|
|
207
|
+
match_points = np.where(result >= threshold)
|
|
208
|
+
scores = result[match_points]
|
|
209
|
+
sorted_indices = np.argsort(-scores) # 降序排序
|
|
210
|
+
|
|
211
|
+
for idx in sorted_indices:
|
|
212
|
+
y, x = match_points[0][idx], match_points[1][idx]
|
|
213
|
+
score = float(scores[idx])
|
|
214
|
+
|
|
215
|
+
# 去重
|
|
216
|
+
if remove_duplicate:
|
|
217
|
+
# 获取匹配区域的中心点
|
|
218
|
+
center_x = x + w // 2
|
|
219
|
+
center_y = y + h // 2
|
|
220
|
+
|
|
221
|
+
# 如果中心点已被标记,跳过此匹配
|
|
222
|
+
if used_mask[center_y, center_x] == 255:
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
# 标记整个匹配区域
|
|
226
|
+
used_mask[y:y+h, x:x+w] = 255
|
|
227
|
+
|
|
228
|
+
# 颜色匹配
|
|
229
|
+
if colored:
|
|
230
|
+
img1, img2 = image[y:y+h, x:x+w], template
|
|
231
|
+
if mask is not None:
|
|
232
|
+
# 如果用了 Mask,需要裁剪出 Mask 区域,其余部分置黑
|
|
233
|
+
img1 = cv2.bitwise_and(img1, img1, mask=mask)
|
|
234
|
+
img2 = cv2.bitwise_and(img2, img2, mask=mask)
|
|
235
|
+
|
|
236
|
+
if not hist_match(img1, img2, (0, 0, w, h)):
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
matches.append(TemplateMatchResult(
|
|
240
|
+
score=score,
|
|
241
|
+
position=KbPoint(int(x) + (rect.x1 if rect else 0), int(y) + (rect.y1 if rect else 0)),
|
|
242
|
+
size=KbSize(int(w), int(h))
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
# 如果达到最大结果数,提前结束
|
|
246
|
+
if max_results > 0 and len(matches) >= max_results:
|
|
247
|
+
break
|
|
248
|
+
|
|
249
|
+
return matches
|
|
250
|
+
|
|
251
|
+
def hist_match(
|
|
252
|
+
image: MatLike | str,
|
|
253
|
+
template: MatLike | str,
|
|
254
|
+
rect: CvRect | None = None,
|
|
255
|
+
threshold: float = 0.9,
|
|
256
|
+
) -> bool:
|
|
257
|
+
"""
|
|
258
|
+
对输入图像的矩形部分与模板进行颜色直方图匹配。
|
|
259
|
+
将图像分为上中下三个区域,分别计算直方图并比较相似度。
|
|
260
|
+
|
|
261
|
+
https://answers.opencv.org/question/59027/template-matching-using-color/
|
|
262
|
+
|
|
263
|
+
:param image: 输入图像
|
|
264
|
+
:param template: 模板图像
|
|
265
|
+
:param rect: 输入图像中待匹配的矩形区域
|
|
266
|
+
:param threshold: 相似度阈值,默认为 0.8
|
|
267
|
+
:return: 是否匹配成功
|
|
268
|
+
"""
|
|
269
|
+
# 统一参数
|
|
270
|
+
image = unify_image(image)
|
|
271
|
+
template = unify_image(template)
|
|
272
|
+
|
|
273
|
+
# 从图像中裁剪出矩形区域
|
|
274
|
+
if rect is None:
|
|
275
|
+
roi = image
|
|
276
|
+
else:
|
|
277
|
+
x, y, w, h = rect
|
|
278
|
+
roi = image[y:y+h, x:x+w]
|
|
279
|
+
|
|
280
|
+
# 确保尺寸一致
|
|
281
|
+
if roi.shape != template.shape:
|
|
282
|
+
# roi = cv2.resize(roi, (template.shape[1], template.shape[0]))
|
|
283
|
+
raise ValueError('Expected two images with the same size.')
|
|
284
|
+
|
|
285
|
+
# 将图像分为上中下三个区域
|
|
286
|
+
h = roi.shape[0]
|
|
287
|
+
h_band = h // 3
|
|
288
|
+
bands_roi = [
|
|
289
|
+
roi[0:h_band],
|
|
290
|
+
roi[h_band:2*h_band],
|
|
291
|
+
roi[2*h_band:h]
|
|
292
|
+
]
|
|
293
|
+
bands_template = [
|
|
294
|
+
template[0:h_band],
|
|
295
|
+
template[h_band:2*h_band],
|
|
296
|
+
template[2*h_band:h]
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
# 计算每个区域的直方图
|
|
300
|
+
total_score = 0
|
|
301
|
+
for roi_band, template_band in zip(bands_roi, bands_template):
|
|
302
|
+
hist_roi = cv2.calcHist([roi_band], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256])
|
|
303
|
+
hist_template = cv2.calcHist([template_band], [0, 1, 2], None, [8, 8, 8], [0, 256, 0, 256, 0, 256])
|
|
304
|
+
|
|
305
|
+
# 归一化直方图
|
|
306
|
+
cv2.normalize(hist_roi, hist_roi)
|
|
307
|
+
cv2.normalize(hist_template, hist_template)
|
|
308
|
+
|
|
309
|
+
# 计算直方图相似度
|
|
310
|
+
score = cv2.compareHist(hist_roi, hist_template, cv2.HISTCMP_CORREL)
|
|
311
|
+
total_score += score
|
|
312
|
+
|
|
313
|
+
# 计算平均相似度
|
|
314
|
+
avg_score = total_score / 3
|
|
315
|
+
return avg_score >= threshold
|
|
316
|
+
|
|
317
|
+
def find_all_crop(
|
|
318
|
+
image: MatLike | str | Image,
|
|
319
|
+
template: MatLike | str | Image,
|
|
320
|
+
mask: MatLike | str | Image | None = None,
|
|
321
|
+
transparent: bool = False,
|
|
322
|
+
threshold: float = 0.8,
|
|
323
|
+
*,
|
|
324
|
+
rect: KbRect | None = None,
|
|
325
|
+
colored: bool = False,
|
|
326
|
+
remove_duplicate: bool = True,
|
|
327
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
328
|
+
) -> list[CropResult]:
|
|
329
|
+
"""
|
|
330
|
+
指定一个模板,在输入图像中寻找其出现的所有位置,并裁剪出结果。
|
|
331
|
+
|
|
332
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
333
|
+
:param template: 模板图像,可以是图像路径或 cv2.Mat。
|
|
334
|
+
:param mask: 掩码图像,可以是图像路径或 cv2.Mat。
|
|
335
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
336
|
+
:param threshold: 阈值,默认为 0.8。
|
|
337
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
338
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
339
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
340
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
341
|
+
"""
|
|
342
|
+
matches = template_match(
|
|
343
|
+
template,
|
|
344
|
+
image,
|
|
345
|
+
mask,
|
|
346
|
+
rect=rect,
|
|
347
|
+
transparent=transparent,
|
|
348
|
+
threshold=threshold,
|
|
349
|
+
max_results=-1,
|
|
350
|
+
remove_duplicate=remove_duplicate,
|
|
351
|
+
colored=colored,
|
|
352
|
+
preprocessors=preprocessors,
|
|
353
|
+
)
|
|
354
|
+
# logger.debug(
|
|
355
|
+
# f'find_all_crop(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
|
356
|
+
# f'matches: {_results2str(matches)}'
|
|
357
|
+
# )
|
|
358
|
+
return [CropResult(
|
|
359
|
+
match.score,
|
|
360
|
+
match.position,
|
|
361
|
+
match.size,
|
|
362
|
+
image[match.rect[1]:match.rect[1]+match.rect[3], match.rect[0]:match.rect[0]+match.rect[2]] # type: ignore
|
|
363
|
+
) for match in matches]
|
|
364
|
+
|
|
365
|
+
def find(
|
|
366
|
+
image: MatLike,
|
|
367
|
+
template: MatLike | str | Image,
|
|
368
|
+
mask: MatLike | str | Image | None = None,
|
|
369
|
+
*,
|
|
370
|
+
rect: KbRect | None = None,
|
|
371
|
+
transparent: bool = False,
|
|
372
|
+
threshold: float = 0.8,
|
|
373
|
+
debug_output: bool = True,
|
|
374
|
+
colored: bool = False,
|
|
375
|
+
remove_duplicate: bool = True,
|
|
376
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
377
|
+
) -> TemplateMatchResult | None:
|
|
378
|
+
"""
|
|
379
|
+
指定一个模板,在输入图像中寻找其出现的第一个位置。
|
|
380
|
+
|
|
381
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
382
|
+
:param template: 模板图像,可以是图像路径或 cv2.Mat。
|
|
383
|
+
:param mask: 掩码图像,可以是图像路径或 cv2.Mat。
|
|
384
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
385
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
386
|
+
:param threshold: 阈值,默认为 0.8。
|
|
387
|
+
:param debug_output: 是否输出调试信息,默认为 True。
|
|
388
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
389
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
390
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
391
|
+
"""
|
|
392
|
+
matches = template_match(
|
|
393
|
+
template,
|
|
394
|
+
image,
|
|
395
|
+
mask,
|
|
396
|
+
rect=rect,
|
|
397
|
+
transparent=transparent,
|
|
398
|
+
threshold=threshold,
|
|
399
|
+
max_results=1,
|
|
400
|
+
remove_duplicate=remove_duplicate,
|
|
401
|
+
colored=colored,
|
|
402
|
+
preprocessors=preprocessors,
|
|
403
|
+
)
|
|
404
|
+
# logger.debug(
|
|
405
|
+
# f'find(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
|
406
|
+
# f'matches: {_results2str(matches)}'
|
|
407
|
+
# )
|
|
408
|
+
# 调试输出
|
|
409
|
+
if debug.enabled and debug_output:
|
|
410
|
+
result_image = _draw_result(image, matches)
|
|
411
|
+
result_text = f"template: {img(template)} \n"
|
|
412
|
+
result_text += f"matches: {len(matches)} \n"
|
|
413
|
+
for match in matches:
|
|
414
|
+
result_text += f"score: {match.score} position: {match.position} size: {match.size} \n"
|
|
415
|
+
debug_result(
|
|
416
|
+
'image.find',
|
|
417
|
+
[result_image, image],
|
|
418
|
+
result_text
|
|
419
|
+
)
|
|
420
|
+
return matches[0] if len(matches) > 0 else None
|
|
421
|
+
|
|
422
|
+
def find_all(
|
|
423
|
+
image: MatLike,
|
|
424
|
+
template: MatLike | str | Image,
|
|
425
|
+
mask: MatLike | str | Image | None = None,
|
|
426
|
+
*,
|
|
427
|
+
rect: KbRect | None = None,
|
|
428
|
+
transparent: bool = False,
|
|
429
|
+
threshold: float = 0.8,
|
|
430
|
+
remove_duplicate: bool = True,
|
|
431
|
+
colored: bool = False,
|
|
432
|
+
debug_output: bool = True,
|
|
433
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
434
|
+
) -> list[TemplateMatchResult]:
|
|
435
|
+
"""
|
|
436
|
+
指定一个模板,在输入图像中寻找其出现的所有位置。
|
|
437
|
+
|
|
438
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
439
|
+
:param template: 模板图像,可以是图像路径或 cv2.Mat。
|
|
440
|
+
:param mask: 掩码图像,可以是图像路径或 cv2.Mat。
|
|
441
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
442
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
443
|
+
:param threshold: 阈值,默认为 0.8。
|
|
444
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
445
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
446
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
447
|
+
"""
|
|
448
|
+
results = template_match(
|
|
449
|
+
template,
|
|
450
|
+
image,
|
|
451
|
+
mask,
|
|
452
|
+
rect=rect,
|
|
453
|
+
transparent=transparent,
|
|
454
|
+
threshold=threshold,
|
|
455
|
+
max_results=-1,
|
|
456
|
+
remove_duplicate=remove_duplicate,
|
|
457
|
+
colored=colored,
|
|
458
|
+
preprocessors=preprocessors,
|
|
459
|
+
)
|
|
460
|
+
# logger.debug(
|
|
461
|
+
# f'find_all(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
|
462
|
+
# f'matches: {_results2str(results)}'
|
|
463
|
+
# )
|
|
464
|
+
if debug.enabled and debug_output:
|
|
465
|
+
result_image = _draw_result(image, results)
|
|
466
|
+
debug_result(
|
|
467
|
+
'image.find_all',
|
|
468
|
+
[result_image, image],
|
|
469
|
+
f"template: {img(template)} \n"
|
|
470
|
+
f"matches: {len(results)} \n"
|
|
471
|
+
)
|
|
472
|
+
return results
|
|
473
|
+
|
|
474
|
+
def find_multi(
|
|
475
|
+
image: MatLike,
|
|
476
|
+
templates: Sequence[MatLike | str | Image],
|
|
477
|
+
masks: Sequence[MatLike | str | Image | None] | None = None,
|
|
478
|
+
*,
|
|
479
|
+
rect: KbRect | None = None,
|
|
480
|
+
transparent: bool = False,
|
|
481
|
+
threshold: float = 0.8,
|
|
482
|
+
colored: bool = False,
|
|
483
|
+
remove_duplicate: bool = True,
|
|
484
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
485
|
+
) -> MultipleTemplateMatchResult | None:
|
|
486
|
+
"""
|
|
487
|
+
指定多个模板,在输入图像中逐个寻找模板,返回第一个匹配到的结果。
|
|
488
|
+
|
|
489
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
490
|
+
:param templates: 模板图像列表,可以是图像路径或 cv2.Mat。
|
|
491
|
+
:param masks: 掩码图像列表,可以是图像路径或 cv2.Mat。
|
|
492
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
493
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
494
|
+
:param threshold: 阈值,默认为 0.8。
|
|
495
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
496
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
497
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
498
|
+
"""
|
|
499
|
+
ret = None
|
|
500
|
+
if masks is None:
|
|
501
|
+
_masks = [None] * len(templates)
|
|
502
|
+
else:
|
|
503
|
+
_masks = masks
|
|
504
|
+
for index, (template, mask) in enumerate(zip(templates, _masks)):
|
|
505
|
+
find_result = find(
|
|
506
|
+
image,
|
|
507
|
+
template,
|
|
508
|
+
mask,
|
|
509
|
+
rect=rect,
|
|
510
|
+
transparent=transparent,
|
|
511
|
+
threshold=threshold,
|
|
512
|
+
colored=colored,
|
|
513
|
+
debug_output=False,
|
|
514
|
+
remove_duplicate=remove_duplicate,
|
|
515
|
+
preprocessors=preprocessors,
|
|
516
|
+
)
|
|
517
|
+
# 调试输出
|
|
518
|
+
if find_result is not None:
|
|
519
|
+
ret = MultipleTemplateMatchResult(
|
|
520
|
+
score=find_result.score,
|
|
521
|
+
position=find_result.position,
|
|
522
|
+
size=find_result.size,
|
|
523
|
+
index=index
|
|
524
|
+
)
|
|
525
|
+
break
|
|
526
|
+
# logger.debug(
|
|
527
|
+
# f'find_multi(): templates: {_imgs2str(templates)} images: {_img2str(image)} masks: {_imgs2str(masks)} '
|
|
528
|
+
# f'result: {_result2str(ret)}'
|
|
529
|
+
# )
|
|
530
|
+
if debug.enabled:
|
|
531
|
+
msg = (
|
|
532
|
+
"<table class='result-table'>" +
|
|
533
|
+
"<tr><th>Template</th><th>Mask</th><th>Result</th></tr>" +
|
|
534
|
+
"\n".join([
|
|
535
|
+
f"<tr><td>{img(t)}</td><td>{img(m)}</td><td>{'✓' if ret and t == templates[ret.index] else '✗'}</td></tr>"
|
|
536
|
+
for i, (t, m) in enumerate(zip(templates, _masks))
|
|
537
|
+
]) +
|
|
538
|
+
"</table>\n"
|
|
539
|
+
)
|
|
540
|
+
debug_result(
|
|
541
|
+
'image.find_multi',
|
|
542
|
+
[_draw_result(image, ret), image],
|
|
543
|
+
msg
|
|
544
|
+
)
|
|
545
|
+
return ret
|
|
546
|
+
|
|
547
|
+
def find_all_multi(
|
|
548
|
+
image: MatLike,
|
|
549
|
+
templates: list[MatLike | str | Image],
|
|
550
|
+
masks: list[MatLike | str | Image | None] | None = None,
|
|
551
|
+
*,
|
|
552
|
+
rect: KbRect | None = None,
|
|
553
|
+
transparent: bool = False,
|
|
554
|
+
threshold: float = 0.8,
|
|
555
|
+
colored: bool = False,
|
|
556
|
+
remove_duplicate: bool = True,
|
|
557
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
558
|
+
) -> list[MultipleTemplateMatchResult]:
|
|
559
|
+
"""
|
|
560
|
+
指定多个模板,在输入图像中逐个寻找模板,返回所有匹配到的结果。
|
|
561
|
+
|
|
562
|
+
此函数等价于
|
|
563
|
+
```python
|
|
564
|
+
result = []
|
|
565
|
+
for template in templates:
|
|
566
|
+
result.append(find_all(template, ...))
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
570
|
+
:param templates: 模板图像列表,可以是图像路径或 cv2.Mat。
|
|
571
|
+
:param masks: 掩码图像列表,可以是图像路径或 cv2.Mat。
|
|
572
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
573
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
574
|
+
:param threshold: 阈值,默认为 0.8。
|
|
575
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
576
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
577
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
578
|
+
:return: 匹配到的一维结果列表。
|
|
579
|
+
"""
|
|
580
|
+
ret: list[MultipleTemplateMatchResult] = []
|
|
581
|
+
if masks is None:
|
|
582
|
+
_masks = [None] * len(templates)
|
|
583
|
+
else:
|
|
584
|
+
_masks = masks
|
|
585
|
+
|
|
586
|
+
for index, (template, mask) in enumerate(zip(templates, _masks)):
|
|
587
|
+
results = find_all(
|
|
588
|
+
image,
|
|
589
|
+
template,
|
|
590
|
+
mask,
|
|
591
|
+
rect=rect,
|
|
592
|
+
transparent=transparent,
|
|
593
|
+
threshold=threshold,
|
|
594
|
+
colored=colored,
|
|
595
|
+
remove_duplicate=remove_duplicate,
|
|
596
|
+
debug_output=False,
|
|
597
|
+
preprocessors=preprocessors,
|
|
598
|
+
)
|
|
599
|
+
ret.extend([
|
|
600
|
+
MultipleTemplateMatchResult.from_template_match_result(r, index)
|
|
601
|
+
for r in results
|
|
602
|
+
])
|
|
603
|
+
# logger.debug(
|
|
604
|
+
# f'find_all_multi(): templates: {_imgs2str(templates)} images: {_img2str(image)} masks: {_imgs2str(masks)} '
|
|
605
|
+
# f'result: {_results2str(ret)}'
|
|
606
|
+
# )
|
|
607
|
+
if debug.enabled:
|
|
608
|
+
# 参数表格
|
|
609
|
+
msg = (
|
|
610
|
+
"<center>Templates</center>"
|
|
611
|
+
"<table class='result-table'>"
|
|
612
|
+
"<tr><th>Template</th><th>Mask</th></tr>"
|
|
613
|
+
)
|
|
614
|
+
for t, m in zip(templates, _masks):
|
|
615
|
+
msg += f"<tr><td>{img(t)}</td><td>{img(m)}</td></tr>"
|
|
616
|
+
msg += "</table>"
|
|
617
|
+
msg += "<br>"
|
|
618
|
+
# 结果表格
|
|
619
|
+
msg += (
|
|
620
|
+
"<center>Results</center>"
|
|
621
|
+
"<table class='result-table'>"
|
|
622
|
+
"<tr><th>Template</th><th>Mask</th><th>Result</th></tr>"
|
|
623
|
+
)
|
|
624
|
+
for result in ret:
|
|
625
|
+
template = templates[result.index]
|
|
626
|
+
mask = _masks[result.index]
|
|
627
|
+
msg += f"<tr><td>{img(template)}</td><td>{img(mask)}</td><td>{result.position}</td></tr>"
|
|
628
|
+
msg += "</table>"
|
|
629
|
+
debug_result(
|
|
630
|
+
'image.find_all_multi',
|
|
631
|
+
[_draw_result(image, ret), image],
|
|
632
|
+
msg
|
|
633
|
+
)
|
|
634
|
+
return ret
|
|
635
|
+
|
|
636
|
+
def count(
|
|
637
|
+
image: MatLike,
|
|
638
|
+
template: MatLike | str | Image,
|
|
639
|
+
mask: MatLike | str | Image | None = None,
|
|
640
|
+
*,
|
|
641
|
+
rect: KbRect | None = None,
|
|
642
|
+
transparent: bool = False,
|
|
643
|
+
threshold: float = 0.8,
|
|
644
|
+
remove_duplicate: bool = True,
|
|
645
|
+
colored: bool = False,
|
|
646
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
647
|
+
) -> int:
|
|
648
|
+
"""
|
|
649
|
+
指定一个模板,统计其出现的次数。
|
|
650
|
+
|
|
651
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
652
|
+
:param template: 模板图像,可以是图像路径或 cv2.Mat。
|
|
653
|
+
:param mask: 掩码图像,可以是图像路径或 cv2.Mat。
|
|
654
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
655
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
656
|
+
:param threshold: 阈值,默认为 0.8。
|
|
657
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
658
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
659
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
660
|
+
"""
|
|
661
|
+
results = template_match(
|
|
662
|
+
template,
|
|
663
|
+
image,
|
|
664
|
+
mask,
|
|
665
|
+
rect=rect,
|
|
666
|
+
transparent=transparent,
|
|
667
|
+
threshold=threshold,
|
|
668
|
+
max_results=-1,
|
|
669
|
+
remove_duplicate=remove_duplicate,
|
|
670
|
+
colored=colored,
|
|
671
|
+
preprocessors=preprocessors,
|
|
672
|
+
)
|
|
673
|
+
# logger.debug(
|
|
674
|
+
# f'count(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
|
675
|
+
# f'result: {_results2str(results)}'
|
|
676
|
+
# )
|
|
677
|
+
if debug.enabled:
|
|
678
|
+
result_image = _draw_result(image, results)
|
|
679
|
+
debug_result(
|
|
680
|
+
'image.count',
|
|
681
|
+
[result_image, image],
|
|
682
|
+
(
|
|
683
|
+
f"template: {img(template)} \n"
|
|
684
|
+
f"mask: {img(mask)} \n"
|
|
685
|
+
f"transparent: {transparent} \n"
|
|
686
|
+
f"threshold: {threshold} \n"
|
|
687
|
+
f"count: {len(results)} \n"
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
return len(results)
|
|
691
|
+
|
|
692
|
+
def expect(
|
|
693
|
+
image: MatLike,
|
|
694
|
+
template: MatLike | str | Image,
|
|
695
|
+
mask: MatLike | str | Image | None = None,
|
|
696
|
+
*,
|
|
697
|
+
rect: KbRect | None = None,
|
|
698
|
+
transparent: bool = False,
|
|
699
|
+
threshold: float = 0.8,
|
|
700
|
+
colored: bool = False,
|
|
701
|
+
remove_duplicate: bool = True,
|
|
702
|
+
preprocessors: list[PreprocessorProtocol] | None = None,
|
|
703
|
+
) -> TemplateMatchResult:
|
|
704
|
+
"""
|
|
705
|
+
指定一个模板,寻找其出现的第一个位置。若未找到,则抛出异常。
|
|
706
|
+
|
|
707
|
+
:param image: 图像,可以是图像路径或 cv2.Mat。
|
|
708
|
+
:param template: 模板图像,可以是图像路径或 cv2.Mat。
|
|
709
|
+
:param mask: 掩码图像,可以是图像路径或 cv2.Mat。
|
|
710
|
+
:param rect: 如果指定,则只在指定矩形区域内进行匹配。
|
|
711
|
+
:param transparent: 若为 True,则认为输入模板是透明的,并自动将透明模板转换为 Mask 图像。
|
|
712
|
+
:param threshold: 阈值,默认为 0.8。
|
|
713
|
+
:param colored: 是否匹配颜色,默认为 False。
|
|
714
|
+
:param remove_duplicate: 是否移除重复结果,默认为 True。
|
|
715
|
+
:param preprocessors: 预处理列表,默认为 None。
|
|
716
|
+
"""
|
|
717
|
+
ret = find(
|
|
718
|
+
image,
|
|
719
|
+
template,
|
|
720
|
+
mask,
|
|
721
|
+
rect=rect,
|
|
722
|
+
transparent=transparent,
|
|
723
|
+
threshold=threshold,
|
|
724
|
+
colored=colored,
|
|
725
|
+
remove_duplicate=remove_duplicate,
|
|
726
|
+
debug_output=False,
|
|
727
|
+
preprocessors=preprocessors,
|
|
728
|
+
)
|
|
729
|
+
# logger.debug(
|
|
730
|
+
# f'expect(): template: {_img2str(template)} image: {_img2str(image)} mask: {_img2str(mask)} '
|
|
731
|
+
# f'result: {_result2str(ret)}'
|
|
732
|
+
# )
|
|
733
|
+
if debug.enabled:
|
|
734
|
+
debug_result(
|
|
735
|
+
'image.expect',
|
|
736
|
+
[_draw_result(image, ret), image],
|
|
737
|
+
(
|
|
738
|
+
f"template: {img(template)} \n"
|
|
739
|
+
f"mask: {img(mask)} \n"
|
|
740
|
+
f"args: transparent={transparent} threshold={threshold} \n"
|
|
741
|
+
f"result: {ret} "
|
|
742
|
+
'<span class="text-success">SUCCESS</span>' if ret is not None
|
|
743
|
+
else '<span class="text-danger">FAILED</span>'
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
if ret is None:
|
|
747
|
+
raise TemplateNoMatchError(image, template)
|
|
748
|
+
else:
|
|
749
|
+
return ret
|
|
750
|
+
|
|
751
|
+
def similar(
|
|
752
|
+
image1: MatLike,
|
|
753
|
+
image2: MatLike,
|
|
754
|
+
threshold: float = 0.9
|
|
755
|
+
) -> bool:
|
|
756
|
+
"""
|
|
757
|
+
判断两张图像是否相似(灰度)。输入的两张图片必须为相同尺寸。
|
|
758
|
+
"""
|
|
759
|
+
if image1.shape != image2.shape:
|
|
760
|
+
raise ValueError('Expected two images with the same size.')
|
|
761
|
+
image1 = cv2.cvtColor(image1, cv2.COLOR_BGR2GRAY)
|
|
762
|
+
image2 = cv2.cvtColor(image2, cv2.COLOR_BGR2GRAY)
|
|
763
|
+
result = structural_similarity(image1, image2, multichannel=True)
|
|
764
|
+
# logger.debug(
|
|
765
|
+
# f'similar(): image1: {_img2str(image1)} image2: {_img2str(image2)} '
|
|
766
|
+
# f'result: {result}'
|
|
767
|
+
# )
|
|
768
|
+
# 调试输出
|
|
769
|
+
if debug.enabled:
|
|
770
|
+
result_image = np.hstack([image1, image2])
|
|
771
|
+
debug_result(
|
|
772
|
+
'image.similar',
|
|
773
|
+
[result_image, image1, image2],
|
|
774
|
+
f"result: {result} >= {threshold} == {result >= threshold} \n"
|
|
775
|
+
)
|
|
776
|
+
return result >= threshold
|
|
777
|
+
|
|
778
|
+
|