refactor(task): 使用基础类中的矩形与点重构任务
This commit is contained in:
parent
2999367415
commit
b434278e4e
|
@ -19,5 +19,6 @@
|
|||
".venv",
|
||||
"venv",
|
||||
"**/node_modules"
|
||||
]
|
||||
],
|
||||
"python.analysis.diagnosticMode": "workspace"
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from typing import Protocol, Literal
|
||||
|
||||
import cv2
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 饮料')
|
||||
|
|
|
@ -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
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
Loading…
Reference in New Issue