refactor(task): 使用基础类中的矩形与点重构任务

This commit is contained in:
XcantloadX 2025-05-12 12:49:57 +08:00
parent 2999367415
commit b434278e4e
32 changed files with 211 additions and 265 deletions

View File

@ -19,5 +19,6 @@
".venv",
"venv",
"**/node_modules"
]
],
"python.analysis.diagnosticMode": "workspace"
}

View File

@ -17,13 +17,11 @@ from .backend.context import (
wait
)
from .util import (
Rect,
cropped,
AdaptiveWait,
Countdown,
Interval,
until,
crop_rect,
)
from .backend.color import (
hsv_cv2web,

View File

@ -7,8 +7,8 @@ import cv2
from cv2.typing import MatLike
from .core import unify_image
from ..primitives import RectTuple, Rect
from .debug import result as debug_result, debug, color as debug_color
from ..util import Rect
RgbColorTuple = tuple[int, int, int]
RgbColorStr = str
@ -131,6 +131,7 @@ def find(
* rgb_dist:
计算图片中每个点的颜色到目标颜色的欧氏距离并以 442 为最大值归一化到 0-1 之间
"""
_rect = rect.xywh if rect else None
ret = None
ret_similarity = 0
found_color = None
@ -167,8 +168,8 @@ def find(
# 寻找结果
matches: np.ndarray = dist <= (1 - threshold)
# 只在rect范围内搜索
if rect is not None:
x, y, w, h = rect
if _rect is not None:
x, y, w, h = _rect
search_area = matches[y:y+h, x:x+w]
if search_area.any():
# 在裁剪区域中找到最小距离的点
@ -199,8 +200,8 @@ def find(
(min(result_image.shape[1], x+20), min(result_image.shape[0], y+20)),
(255, 0, 0), 2)
# 绘制搜索范围
if rect is not None:
x, y, w, h = rect
if _rect is not None:
x, y, w, h = _rect
# 红色圈出rect
cv2.rectangle(result_image, (x, y), (x+w, y+h), (0, 0, 255), 2)
debug_result(
@ -219,7 +220,7 @@ def color_distance_map(
image: MatLike | str,
color: RgbColor,
*,
rect: Rect | None = None,
rect: RectTuple | None = None,
) -> np.ndarray:
"""
计算图像中每个像素点到目标颜色的HSL距离并返回归一化后的距离矩阵
@ -266,7 +267,7 @@ def color_distance_map(
dist = np.sqrt((h_diff * 2)**2 + l_diff**2 + s_diff**2) / np.sqrt(6)
return dist
def _rect_intersection(rect1: Rect, rect2: Rect) -> Rect | None:
def _rect_intersection(rect1: RectTuple, rect2: RectTuple) -> RectTuple | None:
"""
计算两个矩形的交集区域
@ -311,6 +312,7 @@ def find_all(
:param max_results: 最大返回结果数量如果为 None则返回所有结果
:return: 结果列表
"""
_rect: RectTuple | None = rect.xywh if rect is not None else None
# 计算距离矩阵
dist = color_distance_map(image, color)
# 筛选满足要求的点,二值化
@ -319,8 +321,8 @@ def find_all(
def filter_by_point(binary: np.ndarray, target_color: RgbColor, dist: np.ndarray, rect: Rect | None = None, max_results: int | None = None) -> list[FindColorPointResult]:
results = []
if rect is not None:
x, y, w, h = rect
if _rect is not None:
x, y, w, h = _rect
search_area = binary[y:y+h, x:x+w]
local_dist = dist[y:y+h, x:x+w]
@ -370,8 +372,8 @@ def find_all(
assert len(contour_rect) == 4
# 如果指定了rect计算轮廓外接矩形与rect的交集
if rect is not None:
intersection = _rect_intersection(contour_rect, rect)
if _rect is not None:
intersection = _rect_intersection(contour_rect, _rect)
if intersection is None:
continue
@ -433,8 +435,8 @@ def find_all(
(min(result_image.shape[1], x+10), min(result_image.shape[0], y+10)),
(255, 0, 0), 1)
# 绘制搜索范围
if rect is not None:
x, y, w, h = rect
if _rect is not None:
x, y, w, h = _rect
cv2.rectangle(result_image, (x, y), (x+w, y+h), (0, 0, 255), 2)
debug_result(
@ -466,10 +468,11 @@ def dominant_color(
:param count: 提取的颜色数量默认为 1
:param rect: 提取范围如果为 None则在整个图像中提取
"""
_rect: RectTuple | None = rect.xywh if rect is not None else None
# 载入/裁剪图像
img = unify_image(image)
if rect is not None:
x, y, w, h = rect
if _rect is not None:
x, y, w, h = _rect
img = img[y:y+h, x:x+w]
pixels = np.float32(img.reshape(-1, 3))
@ -497,8 +500,8 @@ def dominant_color(
if debug.enabled:
origin_image = unify_image(image)
result_image = origin_image.copy()
if rect is not None:
x, y, w, h = rect
if _rect is not None:
x, y, w, h = _rect
cv2.rectangle(result_image, (x, y), (x + w, y + h), (0, 0, 255), 2)
debug_result(
'color.dominant_color',

View File

@ -25,7 +25,6 @@ import cv2
from cv2.typing import MatLike
from kotonebot.client.device import Device
from kotonebot.util import Rect
import kotonebot.backend.image as raw_image
from kotonebot.backend.image import (
TemplateMatchResult,
@ -52,6 +51,7 @@ from kotonebot.backend.core import Image, HintBox
from kotonebot.errors import KotonebotWarning
from kotonebot.client.factory import DeviceImpl
from kotonebot.backend.preprocessor import PreprocessorProtocol
from kotonebot.primitives import Rect
OcrLanguage = Literal['jp', 'en']
ScreenshotMode = Literal['auto', 'manual', 'manual-inherit']
@ -803,11 +803,12 @@ class Context(Generic[T]):
def config(self) -> 'ContextConfig[T]':
return self.__config
@deprecated('使用 Rect 类的实例方法代替')
def rect_expand(rect: Rect, left: int = 0, top: int = 0, right: int = 0, bottom: int = 0) -> Rect:
"""
向四个方向扩展矩形区域
"""
return (rect[0] - left, rect[1] - top, rect[2] + right + left, rect[3] + bottom + top)
return Rect(rect.x1 - left, rect.y1 - top, rect.w + right + left, rect.h + bottom + top)
def use_screenshot(*args: MatLike | None) -> MatLike:
for img in args:

View File

@ -1,14 +1,13 @@
import logging
from functools import cache
from typing import Callable, overload, TYPE_CHECKING
from typing import Callable
import cv2
from cv2.typing import MatLike
from kotonebot.util import cv2_imread
from kotonebot.primitives import RectTuple, Rect, Point
from kotonebot.errors import ResourceFileMissingError
if TYPE_CHECKING:
from kotonebot.util import Rect
class Ocr:
def __init__(
@ -67,20 +66,7 @@ class Image:
return f'<Image: "{self.name}" at {self.path}>'
class HintBox(tuple[int, int, int, int]):
def __new__(
cls,
x1: int,
y1: int,
x2: int,
y2: int,
*,
source_resolution: tuple[int, int],
):
w = x2 - x1
h = y2 - y1
return super().__new__(cls, [x1, y1, w, h])
class HintBox(Rect):
def __init__(
self,
x1: int,
@ -92,11 +78,7 @@ class HintBox(tuple[int, int, int, int]):
description: str | None = None,
source_resolution: tuple[int, int],
):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.name = name
super().__init__(x1, y1, x2 - x1, y2 - y1, name=name)
self.description = description
self.source_resolution = source_resolution
@ -109,17 +91,12 @@ class HintBox(tuple[int, int, int, int]):
return self.y2 - self.y1
@property
def rect(self) -> 'Rect':
def rect(self) -> RectTuple:
return self.x1, self.y1, self.width, self.height
class HintPoint(tuple[int, int]):
def __new__(cls, x: int, y: int):
return super().__new__(cls, (x, y))
class HintPoint(Point):
def __init__(self, x: int, y: int, *, name: str | None = None, description: str | None = None):
self.x = x
self.y = y
self.name = name
super().__init__(x, y, name=name)
self.description = description
def __repr__(self) -> str:

View File

@ -11,7 +11,7 @@ from typing_extensions import deprecated
from dataclasses import dataclass
from kotonebot.backend.ocr import StringMatchFunction
from kotonebot.util import Rect, is_rect
from kotonebot.primitives import Rect, is_rect
from .core import Image

View File

@ -1,16 +1,16 @@
import os
from logging import getLogger
from typing import NamedTuple, Protocol, TypeVar, Sequence, runtime_checkable
from typing import NamedTuple, Protocol, Sequence, runtime_checkable
import cv2
import numpy as np
from cv2.typing import MatLike, Size
from cv2.typing import MatLike, Rect as CvRect
from skimage.metrics import structural_similarity
from .core import Image, unify_image
from ..util import Rect, Point
from .debug import result as debug_result, debug, img
from .preprocessor import PreprocessorProtocol
from kotonebot.primitives import Point as KbPoint, Rect as KbRect, Size as KbSize
from .debug import result as debug_result, debug, img
logger = getLogger(__name__)
@ -24,46 +24,46 @@ class TemplateNoMatchError(Exception):
@runtime_checkable
class ResultProtocol(Protocol):
@property
def rect(self) -> Rect:
def rect(self) -> KbRect:
"""结果区域。左上角坐标和宽高。"""
...
class TemplateMatchResult(NamedTuple):
score: float
position: Point
position: KbPoint
"""结果位置。左上角坐标。"""
size: Size
size: KbSize
"""输入模板的大小。宽高。"""
@property
def rect(self) -> Rect:
"""结果区域。左上角坐标和宽高。"""
return (self.position[0], self.position[1], self.size[0], self.size[1])
def rect(self) -> KbRect:
"""结果区域。"""
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
@property
def right_bottom(self) -> Point:
def right_bottom(self) -> KbPoint:
"""结果右下角坐标。"""
return (self.position[0] + self.size[0], self.position[1] + self.size[1])
return KbPoint(self.position[0] + self.size[0], self.position[1] + self.size[1])
class MultipleTemplateMatchResult(NamedTuple):
score: float
position: Point
position: KbPoint
"""结果位置。左上角坐标。"""
size: Size
size: KbSize
"""命中模板的大小。宽高。"""
index: int
"""命中模板在列表中的索引。"""
@property
def rect(self) -> Rect:
def rect(self) -> KbRect:
"""结果区域。左上角坐标和宽高。"""
return (self.position[0], self.position[1], self.size[0], self.size[1])
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
@property
def right_bottom(self) -> Point:
def right_bottom(self) -> KbPoint:
"""结果右下角坐标。"""
return (self.position[0] + self.size[0], self.position[1] + self.size[1])
return KbPoint(self.position[0] + self.size[0], self.position[1] + self.size[1])
@classmethod
def from_template_match_result(cls, result: TemplateMatchResult, index: int):
@ -76,13 +76,13 @@ class MultipleTemplateMatchResult(NamedTuple):
class CropResult(NamedTuple):
score: float
position: Point
size: Size
position: KbPoint
size: KbSize
image: MatLike
@property
def rect(self) -> Rect:
return (self.position[0], self.position[1], self.size[0], self.size[1])
def rect(self) -> KbRect:
return KbRect(self.position[0], self.position[1], self.size[0], self.size[1])
def _draw_result(image: MatLike, matches: Sequence[ResultProtocol] | ResultProtocol | None) -> MatLike:
"""在图像上绘制匹配结果的矩形框。"""
@ -92,7 +92,7 @@ def _draw_result(image: MatLike, matches: Sequence[ResultProtocol] | ResultProto
matches = [matches]
result_image = image.copy()
for match in matches:
cv2.rectangle(result_image, match.rect, (0, 0, 255), 2)
cv2.rectangle(result_image, match.rect.xywh, (0, 0, 255), 2)
return result_image
def _img2str(image: MatLike | str | Image | None) -> str:
@ -229,8 +229,8 @@ def template_match(
matches.append(TemplateMatchResult(
score=score,
position=(int(x), int(y)),
size=(int(w), int(h))
position=KbPoint(int(x), int(y)),
size=KbSize(int(w), int(h))
))
# 如果达到最大结果数,提前结束
@ -242,7 +242,7 @@ def template_match(
def hist_match(
image: MatLike | str,
template: MatLike | str,
rect: Rect | None = None,
rect: CvRect | None = None,
threshold: float = 0.9,
) -> bool:
"""

View File

@ -15,8 +15,9 @@ from thefuzz import fuzz as _fuzz
from rapidocr_onnxruntime import RapidOCR
from ..util import lf_path
from ..primitives import Rect, Point
from .core import HintBox, Image, unify_image
from ..util import Rect, lf_path
from .debug import result as debug_result, debug
logger = logging.getLogger(__name__)
@ -65,16 +66,16 @@ class OcrResultList(list[OcrResult]):
将所有识别结果合并为一个大结果
"""
if not self:
return OcrResult('', (0, 0, 0, 0), 0, (0, 0, 0, 0))
return OcrResult('', Rect(0, 0, 0, 0), 0, Rect(0, 0, 0, 0))
text = [r.text for r in self]
confidence = sum(r.confidence for r in self) / len(self)
points = []
for r in self:
points.append((r.rect[0], r.rect[1]))
points.append((r.rect[0] + r.rect[2], r.rect[1]))
points.append((r.rect[0], r.rect[1] + r.rect[3]))
points.append((r.rect[0] + r.rect[2], r.rect[1] + r.rect[3]))
rect = bounding_box(points)
points.append(Point(r.rect.x1, r.rect.y1))
points.append(Point(r.rect.x1 + r.rect.w, r.rect.y1))
points.append(Point(r.rect.x1, r.rect.y1 + r.rect.h))
points.append(Point(r.rect.x1 + r.rect.w, r.rect.y1 + r.rect.h))
rect = Rect(xywh=bounding_box(points))
text = '\n'.join(text)
if remove_newlines:
text = text.replace('\n', '')
@ -245,7 +246,7 @@ def _draw_result(image: 'MatLike', result: list[OcrResult]) -> 'MatLike':
for r in result:
# 画矩形框
draw.rectangle(
[r.rect[0], r.rect[1], r.rect[0] + r.rect[2], r.rect[1] + r.rect[3]],
[r.rect.x1, r.rect.y1, r.rect.x1 + r.rect.w, r.rect.y1 + r.rect.h],
outline=(255, 0, 0),
width=2
)
@ -257,8 +258,8 @@ def _draw_result(image: 'MatLike', result: list[OcrResult]) -> 'MatLike':
text_height = text_bbox[3] - text_bbox[1]
# 计算文本位置
text_x = r.rect[0]
text_y = r.rect[1] - text_height - 5 if r.rect[1] > text_height + 5 else r.rect[1] + r.rect[3] + 5
text_x = r.rect.x1
text_y = r.rect.y1 - text_height - 5 if r.rect.y1 > text_height + 5 else r.rect.y1 + r.rect.h + 5
# 添加padding
padding = 4
@ -313,7 +314,7 @@ class Ocr:
:return: 所有识别结果
"""
if rect is not None:
x, y, w, h = rect
x, y, w, h = rect.xywh
img = img[y:y+h, x:x+w]
original_img = img
if pad:
@ -338,8 +339,8 @@ class Ocr:
# result_rect (x, y, w, h)
if rect is not None:
original_rect = (
result_rect[0] + rect[0] - pos_in_padded_img[0],
result_rect[1] + rect[1] - pos_in_padded_img[1],
result_rect[0] + rect.x1 - pos_in_padded_img[0],
result_rect[1] + rect.y1 - pos_in_padded_img[1],
result_rect[2],
result_rect[3]
)
@ -352,8 +353,8 @@ class Ocr:
confidence = float(r[2])
ret.append(OcrResult(
text=text,
rect=result_rect,
original_rect=original_rect,
rect=Rect(xywh=result_rect),
original_rect=Rect(xywh=original_rect),
confidence=confidence
))
ret = OcrResultList(ret)
@ -392,7 +393,7 @@ class Ocr:
"""
if hint is not None:
warnings.warn("使用 `rect` 参数代替")
if ret := self.find(img, text, rect=hint):
if ret := self.find(img, text, rect=Rect(xywh=hint.rect)):
logger.debug(f"find: {text} SUCCESS [hint={hint}]")
return ret
logger.debug(f"find: {text} FAILED [hint={hint}]")
@ -430,7 +431,7 @@ class Ocr:
# HintBox 处理
if hint is not None:
warnings.warn("使用 `rect` 参数代替")
result = self.find_all(img, texts, rect=hint, pad=pad)
result = self.find_all(img, texts, rect=Rect(xywh=hint.rect), pad=pad)
if all(result):
return result

View File

@ -1,4 +1,3 @@
from typing import Protocol, Literal
import cv2

View File

@ -8,10 +8,10 @@ from adbutils import adb
from cv2.typing import MatLike
from adbutils._device import AdbDevice as AdbUtilsDevice
from kotonebot.backend.core import HintBox
from kotonebot.util import Rect, Point, is_rect, is_point
from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable
from ..backend.debug import result
from kotonebot.backend.core import HintBox
from kotonebot.primitives import Rect, Point, is_point
from .protocol import ClickableObjectProtocol, Commandable, Touchable, Screenshotable
logger = logging.getLogger(__name__)
@ -119,14 +119,7 @@ class Device:
点击屏幕上的某个点
"""
...
@overload
def click(self, hint_box: HintBox) -> None:
"""
点击屏幕上的某个矩形区域
"""
...
@overload
def click(self, rect: Rect) -> None:
"""
@ -136,7 +129,6 @@ class Device:
@overload
def click(self, clickable: ClickableObjectProtocol) -> None:
"""
点击屏幕上的某个可点击对象
"""
@ -147,9 +139,7 @@ class Device:
arg2 = args[1] if len(args) > 1 else None
if arg1 is None:
self.__click_last()
elif isinstance(arg1, HintBox):
self.__click_hint_box(arg1)
elif is_rect(arg1):
elif isinstance(arg1, Rect):
self.__click_rect(arg1)
elif is_point(arg1):
self.__click_point_tuple(arg1)
@ -167,8 +157,8 @@ class Device:
def __click_rect(self, rect: Rect) -> None:
# 从矩形中心的 60% 内部随机选择一点
x = rect[0] + rect[2] // 2 + np.random.randint(-int(rect[2] * 0.3), int(rect[2] * 0.3))
y = rect[1] + rect[3] // 2 + np.random.randint(-int(rect[3] * 0.3), int(rect[3] * 0.3))
x = rect.x1 + rect.w // 2 + np.random.randint(-int(rect.w * 0.3), int(rect.w * 0.3))
y = rect.y1 + rect.h // 2 + np.random.randint(-int(rect.h * 0.3), int(rect.h * 0.3))
x = int(x)
y = int(y)
self.click(x, y)
@ -196,9 +186,6 @@ class Device:
def __click_clickable(self, clickable: ClickableObjectProtocol) -> None:
self.click(clickable.rect)
def __click_hint_box(self, hint_box: HintBox) -> None:
self.click(hint_box.rect)
def click_center(self) -> None:
"""
点击屏幕中心
@ -234,7 +221,7 @@ class Device:
def double_click(self, *args, **kwargs) -> None:
from kotonebot import sleep
arg0 = args[0]
if is_rect(arg0) or isinstance(arg0, ClickableObjectProtocol):
if isinstance(arg0, Rect) or isinstance(arg0, ClickableObjectProtocol):
rect = arg0
interval = kwargs.get('interval', 0.4)
self.click(rect)

View File

@ -1,6 +1,6 @@
import time
import socket
from typing import Protocol, NamedTuple
from typing import Protocol
from dataclasses import dataclass
from adbutils import adb, AdbTimeout, AdbError
@ -130,29 +130,4 @@ class HostProtocol(Protocol):
if __name__ == '__main__':
from . import bluestack_global
from pprint import pprint
logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s][%(levelname)s] %(message)s')
# bluestack_global.
ins = Instance(id='1', name='test', adb_port=5555)
ins.wait_available()
#
# while not tcp_ping('127.0.0.1', 16384):
# print('waiting for bluestacks to start...')
# while True:
# print('connecting to bluestacks...')
# try:
# adb.connect('127.0.0.1:16384', timeout=0.1)
# print('connected to bluestacks')
# if d := adb.device_list()[0]:
# if d.get_state() == 'device':
# if d.shell('getprop sys.boot_completed').strip() == '1':
# if 'launcher' in d.app_current().package:
# break
# except Exception as e:
# print(e)
# time.sleep(0.5)
# time.sleep(1)
# print('bluestacks is ready')
pass

View File

@ -2,7 +2,7 @@ from typing import Protocol, TYPE_CHECKING, runtime_checkable, Literal
from cv2.typing import MatLike
from kotonebot.util import Rect
from kotonebot.primitives import Rect
if TYPE_CHECKING:
from .device import Device

View File

@ -4,7 +4,7 @@ badge 模块,用于关联带附加徽章的 UI。
"""
from typing import Literal, NamedTuple
from kotonebot.util import Rect
from kotonebot.primitives import Rect, RectTuple, PointTuple
BadgeCorner = Literal['lt', 'lm', 'lb', 'rt', 'rm', 'rb', 'mt', 'm', 'mb']
"""
@ -33,11 +33,11 @@ def match(
:return: 匹配结果列表
"""
# 将 rect 转换为中心点
def center(rect: Rect) -> tuple[int, int]:
def center(rect: RectTuple) -> PointTuple:
return rect[0] + rect[2] // 2, rect[1] + rect[3] // 2
# 判断 badge 是否在 object 的指定角落位置
def is_in_corner(obj_rect: Rect, badge_center: tuple[int, int]) -> bool:
def is_in_corner(obj_rect: RectTuple, badge_center: PointTuple) -> bool:
obj_center = center(obj_rect)
x_obj, y_obj = obj_center
x_badge, y_badge = badge_center
@ -72,15 +72,15 @@ def match(
available_badges = badges.copy()
for obj_rect in objects:
obj_center = center(obj_rect)
obj_center = center(obj_rect.xywh)
target_badge = None
min_dist = float('inf')
target_index = -1
# 查找最近的符合条件的徽章
for i, badge_rect in enumerate(available_badges):
badge_center = center(badge_rect)
if is_in_corner(obj_rect, badge_center):
badge_center = center(badge_rect.xywh)
if is_in_corner(obj_rect.xywh, badge_center):
dist = ((badge_center[0] - obj_center[0]) ** 2 + (badge_center[1] - obj_center[1]) ** 2) ** 0.5
if dist < min_dist and dist <= threshold_distance:
min_dist = dist

View File

@ -4,7 +4,7 @@ from cv2.typing import MatLike
from kotonebot import action, color, image
from kotonebot.backend.color import HsvColor
from kotonebot.util import Rect
from kotonebot.primitives import RectTuple, Rect
from kotonebot.backend.core import Image
from kotonebot.backend.preprocessor import HsvColorFilter
@ -20,16 +20,16 @@ def filter_rectangles(
过滤出指定颜色并执行轮廓查找返回符合要求的轮廓的 bound box
返回结果按照 y 坐标排序
"""
img_hsv =cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
white_mask = cv2.inRange(img_hsv, np.array(color_ranges[0]), np.array(color_ranges[1]))
contours, _ = cv2.findContours(white_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
result_rects = []
result_rects: list[Rect] = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# 如果不在指定范围内,跳过
if rect is not None:
rect_x1, rect_y1, rect_w, rect_h = rect
rect_x1, rect_y1, rect_w, rect_h = rect.xywh
rect_x2 = rect_x1 + rect_w
rect_y2 = rect_y1 + rect_h
if not (
@ -42,8 +42,8 @@ def filter_rectangles(
aspect_ratio = w / h
area = cv2.contourArea(contour)
if aspect_ratio >= aspect_ratio_threshold and area >= area_threshold:
result_rects.append((x, y, w, h))
result_rects.sort(key=lambda x: x[1])
result_rects.append(Rect(x, y, w, h))
result_rects.sort(key=lambda x: x.y1)
return result_rects
@action('按钮是否禁用', screenshot_mode='manual-inherit')

View File

@ -2,9 +2,10 @@ from dataclasses import dataclass
from typing import Sequence
from ..tasks import R
from kotonebot.primitives import Rect, RectTuple
from kotonebot.backend.core import HintBox
from kotonebot.backend.color import HsvColor
from kotonebot import action, device, ocr, sleep, Rect
from kotonebot import action, device, ocr, sleep
from .common import filter_rectangles, WHITE_LOW, WHITE_HIGH
@dataclass
@ -103,7 +104,7 @@ class CommuEventButtonUI:
if selected is not None:
result.append(selected)
selected.selected = False
result.sort(key=lambda x: x.rect[1])
result.sort(key=lambda x: x.rect.y1)
return result
@action('交流事件按钮.识别描述', screenshot_mode='manual-inherit')
@ -116,7 +117,7 @@ class CommuEventButtonUI:
"""
img = device.screenshot()
rects = filter_rectangles(img, (WHITE_LOW, WHITE_HIGH), 3, 1000, rect=self.rect)
rects.sort(key=lambda x: x[1])
rects.sort(key=lambda x: x.y1)
# TODO: 这里 rects 可能为空,需要加入判断重试
ocr_result = ocr.raw().ocr(img, rect=rects[0])
return ocr_result.squash().text

View File

@ -7,7 +7,7 @@ from cv2.typing import MatLike
from kotonebot.kaa.tasks import R
from kotonebot.kaa.util import paths
from kotonebot.util import Rect
from kotonebot.primitives import RectTuple, Rect
from kotonebot.kaa.game_ui import Scrollable
from kotonebot import device, action
from kotonebot.kaa.image_db import ImageDatabase, HistDescriptor, FileDataSource
@ -21,7 +21,7 @@ RED_DOT = ((157, 205, 255), (179, 255, 255)) # 红点
ORANGE_SELECT_BORDER = ((9, 50, 106), (19, 255, 255)) # 当前选中的偶像的橙色边框
WHITE_BACKGROUND = ((0, 0, 234), (179, 40, 255)) # 白色背景
def extract_idols(img: MatLike) -> list[Rect]:
def extract_idols(img: MatLike) -> list[RectTuple]:
"""
寻找给定图像中的所有偶像
@ -48,7 +48,7 @@ def extract_idols(img: MatLike) -> list[Rect]:
rects.append((x, y, w, h))
return rects
def display_rects(img: MatLike, rects: list[Rect]) -> MatLike:
def display_rects(img: MatLike, rects: list[RectTuple]) -> MatLike:
"""Draw rectangles on the image and display them."""
result = img.copy()
for rect in rects:
@ -60,7 +60,7 @@ def display_rects(img: MatLike, rects: list[Rect]) -> MatLike:
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)
return result
def draw_idol_preview(img: MatLike, rects: list[Rect], db: ImageDatabase, idol_path: str) -> MatLike:
def draw_idol_preview(img: MatLike, rects: list[RectTuple], db: ImageDatabase, idol_path: str) -> MatLike:
"""
在预览图上绘制所有匹配到的偶像
@ -110,7 +110,7 @@ def idols_db() -> ImageDatabase:
def locate_idol(skin_id: str):
device.screenshot()
logger.info('Locating idol %s', skin_id)
x, y, w, h = R.Produce.BoxIdolOverviewIdols
x, y, w, h = R.Produce.BoxIdolOverviewIdols.xywh
db = idols_db()
sc = Scrollable(color_schema='light')
@ -140,7 +140,7 @@ def locate_idol(skin_id: str):
# 同一张卡升级前后图片不一样index 分别为 0 和 1
if match and match.key.startswith(skin_id):
logger.info('Found idol %s', skin_id)
return rx, ry, rw, rh
return Rect(rx, ry, rw, rh)
return None
# cv2.imshow('Detected Idols', cv2.resize(display_rects(img, rects), (0, 0), fx=0.5, fy=0.5))

View File

@ -4,11 +4,12 @@ from typing import Literal
import cv2
import numpy as np
from cv2.typing import MatLike, Rect
from cv2.typing import MatLike
from kotonebot import device, color, action
from kotonebot import device, action
from kotonebot.primitives import Rect
from kotonebot.backend.core import HintBox
from kotonebot.backend.preprocessor import HsvColorFilter
from kotonebot.primitives.geometry import RectTuple
logger = logging.getLogger(__name__)
@ -77,7 +78,7 @@ def find_scroll_bar2(img: MatLike) -> Rect | None:
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 找出最可能是滚动条的轮廓:
# 宽高比 < 0.5,且形似矩形,且最长
rects = []
rects: list[RectTuple] = []
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
contour_area = cv2.contourArea(contour)
@ -86,7 +87,7 @@ def find_scroll_bar2(img: MatLike) -> Rect | None:
rects.append((x, y, w, h))
if rects:
longest_rect = max(rects, key=lambda r: r[2] * r[3])
return longest_rect
return Rect(xywh=longest_rect)
return None
class ScrollableIterator:
@ -190,7 +191,7 @@ class Scrollable:
return False
logger.debug('Scrollbar rect found.')
x, y, w, h = self.scrollbar_rect
x, y, w, h = self.scrollbar_rect.xywh
scroll_img = img[y:y+h, x:x+w]
# 灰度、二值化
gray = cv2.cvtColor(scroll_img, cv2.COLOR_BGR2GRAY)

View File

@ -2,6 +2,8 @@
import logging
from kotonebot.kaa.tasks import R
from kotonebot.primitives import Rect
from kotonebot.kaa.common import conf, Priority
from ..actions.loading import wait_loading_end
from ..actions.scenes import at_home, goto_home
@ -70,8 +72,8 @@ def claim_pass_reward():
# [screenshots/mission/daily.png]
pass_rect = image.expect_wait(R.Daily.ButtonIconPass, timeout=1).rect
# 向右扩展 150px向上扩展 35px
color_rect = (pass_rect[0], pass_rect[1] - 35, pass_rect[2] + 150, pass_rect[3] + 35)
if not color.find('#ff1249', rect=color_rect):
color_rect = (pass_rect.x1, pass_rect.y1 - 35, pass_rect.w + 150, pass_rect.h + 35)
if not color.find('#ff1249', rect=Rect(xywh=color_rect)):
logger.info('No pass reward to claim.')
return
logger.info('Claiming pass reward.')
@ -130,8 +132,6 @@ if __name__ == '__main__':
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] [%(funcName)s] [%(lineno)d] %(message)s')
logging.getLogger('kotonebot').setLevel(logging.DEBUG)
logger.setLevel(logging.DEBUG)
from .common import conf
conf().mission_reward.enabled = True
# if image.find(R.Common.CheckboxUnchecked):
# logger.debug('Checking skip all.')

View File

@ -10,7 +10,8 @@ from kotonebot.kaa.tasks import R
from kotonebot.kaa.common import conf
from kotonebot.kaa.game_ui import dialog
from kotonebot.kaa.util.trace import trace
from kotonebot import action, Interval, Countdown, device, image, sleep, ocr, contains, use_screenshot, color, Rect
from kotonebot.primitives import RectTuple, Rect
from kotonebot import action, Interval, Countdown, device, image, sleep, ocr, contains, use_screenshot, color
class SkillCard(NamedTuple):
available: bool
@ -159,7 +160,7 @@ def do_cards(
if no_remaining_card and no_card_cd.expired():
logger.debug('No remaining card detected. Skip this turn.')
# TODO: HARD CODEDED
SKIP_POSITION = (621, 739, 85, 85)
SKIP_POSITION = Rect(621, 739, 85, 85)
device.click(SKIP_POSITION)
no_card_cd.reset()
continue
@ -185,7 +186,7 @@ def do_cards(
continue
card_rects = calc_card_position(card_count)
card_rect = card_rects[0]
device.double_click(card_rect[:4])
device.double_click(Rect(xywh=card_rect[:4]))
sleep(2)
timeout_cd.reset()
# 结束条件
@ -277,7 +278,7 @@ def handle_recommended_card(
def skill_card_count(img: MatLike | None = None):
"""获取当前持有的技能卡数量"""
img = use_screenshot(img)
x, y, w, h = R.InPurodyuusu.BoxCardLetter
x, y, w, h = R.InPurodyuusu.BoxCardLetter.xywh
img = img[y:y+h, x:x+w]
count = image.raw().count(img, R.InPurodyuusu.A)
count += image.raw().count(img, R.InPurodyuusu.M)
@ -346,7 +347,7 @@ def detect_recommended_card(
right_score,
top_score,
bottom_score,
(x, y, w, h)
Rect(x, y, w, h)
))
img = original_image.copy()
# cv2.imshow(f"card detect {return_value}", cv2.cvtColor(glow_area, cv2.COLOR_HSV2BGR))
@ -376,7 +377,7 @@ def detect_recommended_card(
)
# 跟踪检测结果
if conf().trace.recommend_card_detection:
x, y, w, h = filtered_results[0].rect
x, y, w, h = filtered_results[0].rect.xywh
cv2.rectangle(original_image, (x, y), (x+w, y+h), (0, 0, 255), 3)
trace('rec-card', original_image, {
'card_count': card_count,

View File

@ -1,7 +1,6 @@
from typing import Literal
from logging import getLogger
from kotonebot.kaa.tasks import R
from kotonebot import (
ocr,
device,
@ -10,6 +9,8 @@ from kotonebot import (
sleep,
Interval,
)
from kotonebot.primitives import Rect
from kotonebot.kaa.tasks import R
from .p_drink import acquire_p_drink
from kotonebot.util import measure_time
from kotonebot.kaa.common import conf
@ -89,9 +90,9 @@ def select_p_item():
# 前置条件 [screenshots/produce/in_produce/claim_p_item.png]
POSTIONS = [
(157, 820, 128, 128), # x, y, w, h
(296, 820, 128, 128),
(435, 820, 128, 128),
Rect(157, 820, 128, 128), # x, y, w, h
Rect(296, 820, 128, 128),
Rect(435, 820, 128, 128),
] # TODO: HARD CODED
device.click(POSTIONS[0])
sleep(0.5)

View File

@ -35,9 +35,10 @@ def handle_sp_lesson():
"""
if (sp := image.find(R.InPurodyuusu.IconSp)) is not None:
# 取 SP 图标中心点向左、向下偏移 30px
rect = sp.rect
pt = (rect[0] + rect[2] // 2, rect[1] + rect[3] // 2)
device.double_click(pt[0] + 30, pt[1] + 30)
# rect = sp.rect
# pt = (rect[0] + rect[2] // 2, rect[1] + rect[3] // 2)
# device.double_click(pt[0] + 30, pt[1] + 30)
device.double_click(*sp.rect.center.offset(30, 30).xy)
return True
else:
return False

View File

@ -1,15 +1,16 @@
from logging import getLogger
from kotonebot.kaa.tasks import R
from kotonebot.primitives import Rect
from kotonebot import device, image, action, sleep
logger = getLogger(__name__)
# 三个饮料的坐标
POSTIONS = [
(157, 820, 128, 128), # x, y, w, h
(296, 820, 128, 128),
(435, 820, 128, 128),
Rect(157, 820, 128, 128), # x, y, w, h
Rect(296, 820, 128, 128),
Rect(435, 820, 128, 128),
] # TODO: HARD CODED
@action('领取 P 饮料')

View File

@ -20,16 +20,16 @@ logger = logging.getLogger(__name__)
Rect = tuple[int, int, int, int]
"""左上X, 左上Y, 宽度, 高度"""
Point = tuple[int, int]
"""X, Y"""
def is_rect(rect: typing.Any) -> TypeGuard[Rect]:
return isinstance(rect, typing.Sequence) and len(rect) == 4 and all(isinstance(i, int) for i in rect)
def is_point(point: typing.Any) -> TypeGuard[Point]:
return isinstance(point, typing.Sequence) and len(point) == 2 and all(isinstance(i, int) for i in point)
# Rect = tuple[int, int, int, int]
# """左上X, 左上Y, 宽度, 高度"""
# Point = tuple[int, int]
# """X, Y"""
#
# def is_rect(rect: typing.Any) -> TypeGuard[Rect]:
# return isinstance(rect, typing.Sequence) and len(rect) == 4 and all(isinstance(i, int) for i in rect)
#
# def is_point(point: typing.Any) -> TypeGuard[Point]:
# return isinstance(point, typing.Sequence) and len(point) == 2 and all(isinstance(i, int) for i in point)
@deprecated('使用 HintBox 类与 Devtool 工具替代')
def crop(img: MatLike, /, x1: float = 0, y1: float = 0, x2: float = 1, y2: float = 1) -> MatLike:
@ -49,16 +49,16 @@ def crop(img: MatLike, /, x1: float = 0, y1: float = 0, x2: float = 1, y2: float
y2_px = int(h * y2)
return img[y1_px:y2_px, x1_px:x2_px]
@deprecated('使用 numpy 的切片替代')
def crop_rect(img: MatLike, rect: Rect) -> MatLike:
"""
按范围裁剪图像
:param img: 图像
:param rect: 裁剪区域
"""
x, y, w, h = rect
return img[y:y+h, x:x+w]
# @deprecated('使用 numpy 的切片替代')
# def crop_rect(img: MatLike, rect: Rect) -> MatLike:
# """
# 按范围裁剪图像。
#
# :param img: 图像
# :param rect: 裁剪区域。
# """
# x, y, w, h = rect
# return img[y:y+h, x:x+w]
class DeviceHookContextManager:
def __init__(

@ -1 +1 @@
Subproject commit 0eed94f23ca1be9c741cdcbb19dfe6b2dd30cf29
Subproject commit a8672a8d834ccb9de3c265d11e9a8c27667ab3df

@ -1 +1 @@
Subproject commit 6c16824c4fd6083066436cef53fad65652c77348
Subproject commit 117c73664ca8c40cc9a3f9d0ac11b66828fd7589

View File

@ -3,6 +3,7 @@ import cv2
from util import BaseTestCase
from kotonebot.backend.color import find, find_all
from kotonebot.primitives import Rect
def _img_rgb_to_bgr(img: np.ndarray) -> np.ndarray:
return cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
@ -185,12 +186,12 @@ class TestColor(BaseTestCase):
self.assertEqual(len(results), 10)
# 测试矩形区域搜索
rect = (28, 934, 170, 170)
rect = Rect(28, 934, 170, 170)
results = find_all(img, '#ff1249', rect=rect, threshold=0.95)
for result in results:
x, y = result.position
self.assertTrue(rect[0] <= x < rect[0] + rect[2])
self.assertTrue(rect[1] <= y < rect[1] + rect[3])
self.assertTrue(rect.x1 <= x < rect.x2)
self.assertTrue(rect.y1 <= y < rect.y2)
# 测试无效输入
# with self.assertRaises(ValueError):

View File

@ -1,10 +1,11 @@
import re
import unittest
from kotonebot.backend.ocr import jp
from kotonebot.backend.ocr import OcrResult, OcrResultList, bounding_box
import cv2
from kotonebot.backend.ocr import jp
from kotonebot.primitives.geometry import Rect
from kotonebot.backend.ocr import OcrResult, OcrResultList, bounding_box
class TestOcr(unittest.TestCase):
def setUp(self):
@ -55,17 +56,17 @@ class TestOcr(unittest.TestCase):
self.assertGreater(len(result), 0)
def test_ocr_rect(self):
result = jp().ocr(self.img, rect=(147, 614, 417, 32), pad=True)
result = jp().ocr(self.img, rect=Rect(147, 614, 417, 32), pad=True)
self.assertEqual(result[0].text, '受け取るPドリンクを選んでください。')
x, y, w, h = result[0].original_rect
x, y, w, h = result[0].original_rect.xywh
self.assertAlmostEqual(x, 147, delta=10)
self.assertAlmostEqual(y, 614, delta=10)
self.assertAlmostEqual(w, 417, delta=10)
self.assertAlmostEqual(h, 32, delta=10)
result = jp().ocr(self.img, rect=(147, 614, 417, 32), pad=False)
result = jp().ocr(self.img, rect=Rect(147, 614, 417, 32), pad=False)
self.assertEqual(result[0].text, '受け取るPドリンクを選んでください。')
x, y, w, h = result[0].original_rect
x, y, w, h = result[0].original_rect.xywh
self.assertAlmostEqual(x, 147, delta=10)
self.assertAlmostEqual(y, 614, delta=10)
self.assertAlmostEqual(w, 417, delta=10)
@ -79,25 +80,25 @@ class TestOcr(unittest.TestCase):
class TestOcrResult(unittest.TestCase):
def test_regex(self):
result = OcrResult(text='123dd4567rr890', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100))
result = OcrResult(text='123dd4567rr890', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100))
self.assertEqual(result.regex(r'\d+'), ['123', '4567', '890'])
self.assertEqual(result.regex(re.compile(r'\d+')), ['123', '4567', '890'])
def test_numbers(self):
result = OcrResult(text='123dd4567rr890', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100))
result = OcrResult(text='123dd4567rr890', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100))
self.assertEqual(result.numbers(), [123, 4567, 890])
result2 = OcrResult(text='aaa', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100))
result2 = OcrResult(text='aaa', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100))
self.assertEqual(result2.numbers(), [])
result3 = OcrResult(text='1234567890', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100))
result3 = OcrResult(text='1234567890', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100))
self.assertEqual(result3.numbers(), [1234567890])
class TestOcrResultList(unittest.TestCase):
def test_list_compatibility(self):
result = OcrResultList([
OcrResult(text='abc', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='def', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='ghi', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='abc', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
OcrResult(text='def', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
OcrResult(text='ghi', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
])
self.assertEqual(result[0].text, 'abc')
@ -122,17 +123,17 @@ class TestOcrResultList(unittest.TestCase):
def test_where(self):
result = OcrResultList([
OcrResult(text='123dd4567rr890', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='aaa', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='1234567890', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='123dd4567rr890', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
OcrResult(text='aaa', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
OcrResult(text='1234567890', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
])
self.assertEqual(result.where(lambda x: x.startswith('123')), [result[0], result[2]])
def test_first(self):
result = OcrResultList([
OcrResult(text='123dd4567rr890', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='aaa', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='1234567890', rect=(0, 0, 100, 100), confidence=0.95, original_rect=(0, 0, 100, 100)),
OcrResult(text='123dd4567rr890', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
OcrResult(text='aaa', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
OcrResult(text='1234567890', rect=Rect(0, 0, 100, 100), confidence=0.95, original_rect=Rect(0, 0, 100, 100)),
])
self.assertEqual(result.first(), result[0])
result2 = OcrResultList()

View File

@ -1,20 +1,21 @@
import unittest
from kotonebot.primitives import Rect
from kotonebot.backend.context import rect_expand
class TestRectExpand(unittest.TestCase):
def test_rect_expand(self):
cases = [
# 基本扩展
((100, 100, 200, 200), (90, 90, 220, 220), {'top': 10, 'right': 10, 'bottom': 10, 'left': 10}),
(Rect(100, 100, 200, 200), Rect(90, 90, 220, 220), {'top': 10, 'right': 10, 'bottom': 10, 'left': 10}),
# 只扩展顶部
((100, 100, 200, 200), (100, 90, 200, 210), {'top': 10}),
(Rect(100, 100, 200, 200), Rect(100, 90, 200, 210), {'top': 10}),
# 只扩展右侧
((100, 100, 200, 200), (100, 100, 210, 200), {'right': 10}),
(Rect(100, 100, 200, 200), Rect(100, 100, 210, 200), {'right': 10}),
# 只扩展底部
((100, 100, 200, 200), (100, 100, 200, 210), {'bottom': 10}),
(Rect(100, 100, 200, 200), Rect(100, 100, 200, 210), {'bottom': 10}),
# 只扩展左侧
((100, 100, 200, 200), (90, 100, 210, 200), {'left': 10}),
(Rect(100, 100, 200, 200), Rect(90, 100, 210, 200), {'left': 10}),
]
for case in cases:
rect, expected, kwargs = case
@ -24,15 +25,15 @@ class TestRectExpand(unittest.TestCase):
def test_rect_expand_with_negative_value(self):
cases = [
# 负值收缩
((100, 100, 200, 200), (110, 110, 180, 180), {'top': -10, 'right': -10, 'bottom': -10, 'left': -10}),
(Rect(100, 100, 200, 200), Rect(110, 110, 180, 180), {'top': -10, 'right': -10, 'bottom': -10, 'left': -10}),
# 只收缩顶部
((100, 100, 200, 200), (100, 110, 200, 190), {'top': -10}),
(Rect(100, 100, 200, 200), Rect(100, 110, 200, 190), {'top': -10}),
# 只收缩右侧
((100, 100, 200, 200), (100, 100, 190, 200), {'right': -10}),
(Rect(100, 100, 200, 200), Rect(100, 100, 190, 200), {'right': -10}),
# 只收缩底部
((100, 100, 200, 200), (100, 100, 200, 190), {'bottom': -10}),
(Rect(100, 100, 200, 200), Rect(100, 100, 200, 190), {'bottom': -10}),
# 只收缩左侧
((100, 100, 200, 200), (110, 100, 190, 200), {'left': -10}),
(Rect(100, 100, 200, 200), Rect(110, 100, 190, 200), {'left': -10}),
]
for case in cases:
rect, expected, kwargs = case

View File

@ -1,8 +1,8 @@
import os
import time
import unittest
import logging
from unittest.mock import Mock, patch, mock_open
import unittest
from typing import Literal
from unittest.mock import Mock, patch
from kotonebot.util import measure_time
class TestMeasureTime(unittest.TestCase):
@ -36,7 +36,7 @@ class TestMeasureTime(unittest.TestCase):
@patch('time.time')
def test_different_log_levels(self, mock_time):
levels = ['debug', 'info', 'warning', 'error', 'critical']
levels: list[Literal['debug', 'info', 'warning', 'error', 'critical']] = ['debug', 'info', 'warning', 'error', 'critical']
for level in levels:
# Reset mock for each iteration with enough values

View File

@ -1,10 +1,11 @@
from unittest import TestCase
from kotonebot.kaa.game_ui.badge import match, BadgeResult
from kotonebot.util import Rect
from kotonebot.primitives import Rect
from kotonebot.kaa.game_ui.badge import match
def rect_from_center(x: int, y: int) -> Rect:
w, h = 20, 20
return x - w // 2, y - h // 2, w, h
return Rect(x - w // 2, y - h // 2, w, h)
class TestBadge(TestCase):
def test_match(self):

View File

@ -26,7 +26,7 @@ class TestTemplateMatch(unittest.TestCase):
def test_basic(self):
result = template_match(self.template, self.image)
# 圈出结果并保存
cv2.rectangle(self.image, result[0].rect, (0, 0, 255), 2)
cv2.rectangle(self.image, result[0].rect.xywh, (0, 0, 255), 2)
save(self.image, 'TestTemplateMatch.basic')
self.assertGreater(len(result), 0)
@ -45,7 +45,7 @@ class TestTemplateMatch(unittest.TestCase):
)
# 圈出结果并保存
for i, r in enumerate(result):
cv2.rectangle(self.image, r.rect, (0, 0, 255), 2)
cv2.rectangle(self.image, r.rect.xywh, (0, 0, 255), 2)
save(self.image, 'TestTemplateMatch.masked')
self.assertEqual(len(result), 3)

View File

@ -45,10 +45,7 @@ class MockDevice(Device):
def click(self, *args, **kwargs):
if len(args) == 0:
if isinstance(self.last_find, ClickableObjectProtocol):
rect = self.last_find.rect
x = (rect[0] + rect[2]) // 2
y = (rect[1] + rect[3]) // 2
self.last_click = (x, y)
self.last_click = self.last_find.rect.center.xy
elif isinstance(self.last_find, tuple) and len(self.last_find) == 2:
self.last_click = self.last_find
else:
@ -59,10 +56,7 @@ class MockDevice(Device):
self.last_click = (x, y)
elif len(args) == 1:
assert isinstance(args[0], ClickableObjectProtocol)
rect = args[0].rect
x = (rect[0] + rect[2] // 2)
y = (rect[1] + rect[3] // 2)
self.last_click = (x, y)
self.last_click = args[0].rect.center.xy
else:
raise ValueError("Invalid arguments")