2023-05-16 18:54:03 +00:00
|
|
|
import module.config.server as server
|
|
|
|
from module.base.decorator import cached_property, del_cached_property
|
2023-05-14 07:48:34 +00:00
|
|
|
from module.base.resource import Resource
|
|
|
|
from module.base.utils import *
|
2023-05-16 18:54:03 +00:00
|
|
|
from module.exception import ScriptError
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Button(Resource):
|
2023-05-16 18:54:03 +00:00
|
|
|
def __init__(self, file, area, search, color, button):
|
|
|
|
"""
|
2023-05-14 07:48:34 +00:00
|
|
|
Args:
|
2023-05-16 18:54:03 +00:00
|
|
|
file: Filepath to an assets
|
|
|
|
area: Area to crop template
|
|
|
|
search: Area to search from, 20px larger than `area` by default
|
|
|
|
color: Average color of assets
|
|
|
|
button: Area to click if assets appears on the image
|
2023-05-14 07:48:34 +00:00
|
|
|
"""
|
2023-05-16 18:54:03 +00:00
|
|
|
self.file: str = file
|
|
|
|
self.area: t.Tuple[int, int, int, int] = area
|
|
|
|
self.search: t.Tuple[int, int, int, int] = search
|
|
|
|
self.color: t.Tuple[int, int, int] = color
|
|
|
|
self._button: t.Tuple[int, int, int, int] = button
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
self.resource_add(self.file)
|
|
|
|
self._button_offset: t.Tuple[int, int] = (0, 0)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
@property
|
|
|
|
def button(self):
|
|
|
|
return area_offset(self._button, self._button_offset)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-09-17 01:34:17 +00:00
|
|
|
def load_offset(self, button):
|
|
|
|
self._button_offset = button._button_offset
|
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def clear_offset(self):
|
|
|
|
self._button_offset = (0, 0)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
@cached_property
|
2023-05-16 18:54:03 +00:00
|
|
|
def image(self):
|
|
|
|
return load_image(self.file, self.area)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def resource_release(self):
|
|
|
|
del_cached_property(self, 'image')
|
|
|
|
self.clear_offset()
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
2023-05-16 18:54:03 +00:00
|
|
|
return self.file
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
__repr__ = __str__
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return str(self) == str(other)
|
|
|
|
|
|
|
|
def __hash__(self):
|
2023-05-16 18:54:03 +00:00
|
|
|
return hash(self.file)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
def __bool__(self):
|
|
|
|
return True
|
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def match_color(self, image, threshold=10) -> bool:
|
|
|
|
"""
|
|
|
|
Check if the button appears on the image, using average color
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
image (np.ndarray): Screenshot.
|
|
|
|
threshold (int): Default to 10.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: True if button appears on screenshot.
|
|
|
|
"""
|
2023-05-16 18:54:03 +00:00
|
|
|
color = get_color(image, self.area)
|
2023-05-14 07:48:34 +00:00
|
|
|
return color_similar(
|
2023-05-16 18:54:03 +00:00
|
|
|
color1=color,
|
2023-05-14 07:48:34 +00:00
|
|
|
color2=self.color,
|
|
|
|
threshold=threshold
|
|
|
|
)
|
|
|
|
|
2023-11-06 14:29:49 +00:00
|
|
|
def match_template(self, image, similarity=0.85, direct_match=False) -> bool:
|
2023-05-14 07:48:34 +00:00
|
|
|
"""
|
2023-05-16 18:54:03 +00:00
|
|
|
Detects assets by template matching.
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
To Some buttons, its location may not be static, `_button_offset` will be set.
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
image: Screenshot.
|
2023-05-16 18:54:03 +00:00
|
|
|
similarity (float): 0-1.
|
2023-11-06 14:29:49 +00:00
|
|
|
direct_match: True to ignore `self.search`
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool.
|
|
|
|
"""
|
2023-11-06 14:29:49 +00:00
|
|
|
if not direct_match:
|
|
|
|
image = crop(image, self.search, copy=False)
|
2023-05-16 18:54:03 +00:00
|
|
|
res = cv2.matchTemplate(self.image, image, cv2.TM_CCOEFF_NORMED)
|
|
|
|
_, sim, _, point = cv2.minMaxLoc(res)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
self._button_offset = np.array(point) + self.search[:2] - self.area[:2]
|
|
|
|
return sim > similarity
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-11-06 14:29:49 +00:00
|
|
|
def match_multi_template(self, image, similarity=0.85, direct_match=False):
|
|
|
|
"""
|
|
|
|
Detects assets by template matching, return multiple reults
|
|
|
|
|
|
|
|
Args:
|
|
|
|
image: Screenshot.
|
|
|
|
similarity (float): 0-1.
|
|
|
|
direct_match: True to ignore `self.search`
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
list:
|
|
|
|
"""
|
|
|
|
if not direct_match:
|
|
|
|
image = crop(image, self.search, copy=False)
|
|
|
|
res = cv2.matchTemplate(self.image, image, cv2.TM_CCOEFF_NORMED)
|
|
|
|
res = cv2.inRange(res, similarity, 1.)
|
|
|
|
try:
|
|
|
|
points = np.array(cv2.findNonZero(res))[:, 0, :]
|
|
|
|
points += self.search[:2]
|
|
|
|
return points.tolist()
|
|
|
|
except IndexError:
|
|
|
|
# Empty result
|
|
|
|
# IndexError: too many indices for array: array is 0-dimensional, but 3 were indexed
|
|
|
|
return []
|
|
|
|
|
|
|
|
def match_template_color(self, image, similarity=0.85, threshold=30, direct_match=False) -> bool:
|
2023-05-16 18:54:03 +00:00
|
|
|
"""
|
|
|
|
Template match first, color match then
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
image: Screenshot.
|
2023-05-16 18:54:03 +00:00
|
|
|
similarity (float): 0-1.
|
|
|
|
threshold (int): Default to 10.
|
2023-11-06 14:29:49 +00:00
|
|
|
direct_match: True to ignore `self.search`
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
Returns:
|
2023-11-06 14:29:49 +00:00
|
|
|
bool.
|
2023-05-14 07:48:34 +00:00
|
|
|
"""
|
2023-11-06 14:29:49 +00:00
|
|
|
matched = self.match_template(image, similarity=similarity, direct_match=direct_match)
|
2023-05-16 18:54:03 +00:00
|
|
|
if not matched:
|
2023-05-14 07:48:34 +00:00
|
|
|
return False
|
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
area = area_offset(self.area, offset=self._button_offset)
|
|
|
|
color = get_color(image, area)
|
|
|
|
return color_similar(
|
|
|
|
color1=color,
|
|
|
|
color2=self.color,
|
|
|
|
threshold=threshold
|
|
|
|
)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
class ButtonWrapper(Resource):
|
|
|
|
def __init__(self, name='MULTI_ASSETS', **kwargs):
|
|
|
|
self.name = name
|
|
|
|
self.data_buttons = kwargs
|
|
|
|
self._matched_button: t.Optional[Button] = None
|
2023-10-17 05:57:03 +00:00
|
|
|
self.resource_add(f'{name}:{next(self.iter_buttons(), None)}')
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def resource_release(self):
|
2023-10-17 05:57:03 +00:00
|
|
|
del_cached_property(self, 'buttons')
|
2023-05-16 18:54:03 +00:00
|
|
|
self._matched_button = None
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
__repr__ = __str__
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def __eq__(self, other):
|
|
|
|
return str(self) == str(other)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.name)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def __bool__(self):
|
|
|
|
return True
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-10-14 18:18:27 +00:00
|
|
|
def iter_buttons(self) -> t.Iterator[Button]:
|
|
|
|
for _, assets in self.data_buttons.items():
|
|
|
|
if isinstance(assets, Button):
|
|
|
|
yield assets
|
|
|
|
elif isinstance(assets, list):
|
|
|
|
for asset in assets:
|
|
|
|
yield asset
|
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
@cached_property
|
|
|
|
def buttons(self) -> t.List[Button]:
|
2023-09-08 14:23:57 +00:00
|
|
|
for trial in [server.lang, 'share', 'cn']:
|
2023-10-14 18:18:27 +00:00
|
|
|
try:
|
|
|
|
assets = self.data_buttons[trial]
|
2023-05-16 18:54:03 +00:00
|
|
|
if isinstance(assets, Button):
|
|
|
|
return [assets]
|
|
|
|
elif isinstance(assets, list):
|
|
|
|
return assets
|
2023-10-14 18:18:27 +00:00
|
|
|
except KeyError:
|
|
|
|
pass
|
2023-05-16 18:54:03 +00:00
|
|
|
|
2023-09-08 14:23:57 +00:00
|
|
|
raise ScriptError(f'ButtonWrapper({self}) on server {server.lang} has no fallback button')
|
2023-05-16 18:54:03 +00:00
|
|
|
|
|
|
|
def match_color(self, image, threshold=10) -> bool:
|
|
|
|
for assets in self.buttons:
|
|
|
|
if assets.match_color(image, threshold=threshold):
|
|
|
|
self._matched_button = assets
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2023-11-06 14:29:49 +00:00
|
|
|
def match_template(self, image, similarity=0.85, direct_match=False) -> bool:
|
2023-05-16 18:54:03 +00:00
|
|
|
for assets in self.buttons:
|
2023-11-06 14:29:49 +00:00
|
|
|
if assets.match_template(image, similarity=similarity, direct_match=direct_match):
|
2023-05-16 18:54:03 +00:00
|
|
|
self._matched_button = assets
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
2023-11-06 14:29:49 +00:00
|
|
|
def match_multi_template(self, image, similarity=0.85, threshold=5, direct_match=False):
|
|
|
|
"""
|
|
|
|
Detects assets by template matching, return multiple results
|
|
|
|
|
|
|
|
Args:
|
|
|
|
image: Screenshot.
|
|
|
|
similarity (float): 0-1.
|
|
|
|
threshold:
|
|
|
|
direct_match: True to ignore `self.search`
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
list[ClickButton]:
|
|
|
|
"""
|
|
|
|
ps = []
|
|
|
|
for assets in self.buttons:
|
|
|
|
ps += assets.match_multi_template(image, similarity=similarity, direct_match=direct_match)
|
|
|
|
if not ps:
|
|
|
|
return []
|
|
|
|
|
|
|
|
from module.base.utils.points import Points
|
|
|
|
ps = Points(ps).group(threshold=threshold)
|
|
|
|
area_list = [area_offset(self.area, p - self.area[:2]) for p in ps]
|
|
|
|
button_list = [area_offset(self.button, p - self.area[:2]) for p in ps]
|
|
|
|
return [
|
|
|
|
ClickButton(area=info[0], button=info[1], name=f'{self.name}_result{i}')
|
|
|
|
for i, info in enumerate(zip(area_list, button_list))
|
|
|
|
]
|
|
|
|
|
|
|
|
def match_template_color(self, image, similarity=0.85, threshold=30, direct_match=False) -> bool:
|
2023-05-16 18:54:03 +00:00
|
|
|
for assets in self.buttons:
|
2023-11-06 14:29:49 +00:00
|
|
|
if assets.match_template_color(
|
|
|
|
image, similarity=similarity, threshold=threshold, direct_match=direct_match):
|
2023-05-16 18:54:03 +00:00
|
|
|
self._matched_button = assets
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def matched_button(self) -> Button:
|
|
|
|
if self._matched_button is None:
|
|
|
|
return self.buttons[0]
|
2023-05-14 07:48:34 +00:00
|
|
|
else:
|
2023-05-16 18:54:03 +00:00
|
|
|
return self._matched_button
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
@property
|
2023-05-30 18:20:45 +00:00
|
|
|
def area(self) -> tuple[int, int, int, int]:
|
2023-05-16 18:54:03 +00:00
|
|
|
return self.matched_button.area
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
@property
|
2023-05-30 18:20:45 +00:00
|
|
|
def search(self) -> tuple[int, int, int, int]:
|
2023-05-16 18:54:03 +00:00
|
|
|
return self.matched_button.search
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
@property
|
2023-05-30 18:20:45 +00:00
|
|
|
def color(self) -> tuple[int, int, int]:
|
2023-05-16 18:54:03 +00:00
|
|
|
return self.matched_button.color
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
@property
|
2023-05-30 18:20:45 +00:00
|
|
|
def button(self) -> tuple[int, int, int, int]:
|
2023-05-16 18:54:03 +00:00
|
|
|
return self.matched_button.button
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-30 18:20:45 +00:00
|
|
|
@property
|
2023-06-13 03:36:54 +00:00
|
|
|
def button_offset(self) -> tuple[int, int]:
|
|
|
|
return self.matched_button._button_offset
|
|
|
|
|
|
|
|
@property
|
2023-05-30 18:20:45 +00:00
|
|
|
def width(self) -> int:
|
|
|
|
return area_size(self.area)[0]
|
|
|
|
|
|
|
|
@property
|
|
|
|
def height(self) -> int:
|
|
|
|
return area_size(self.area)[1]
|
|
|
|
|
2023-09-17 01:34:17 +00:00
|
|
|
def load_offset(self, button):
|
|
|
|
"""
|
|
|
|
Load offset from another button.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
button (Button, ButtonWrapper):
|
|
|
|
"""
|
|
|
|
if isinstance(button, ButtonWrapper):
|
|
|
|
button = button.matched_button
|
2023-10-14 18:18:27 +00:00
|
|
|
for b in self.iter_buttons():
|
2023-09-17 01:34:17 +00:00
|
|
|
b.load_offset(button)
|
|
|
|
|
|
|
|
def clear_offset(self):
|
2023-10-14 18:18:27 +00:00
|
|
|
for b in self.iter_buttons():
|
2023-09-17 01:34:17 +00:00
|
|
|
b.clear_offset()
|
|
|
|
|
2023-10-14 18:18:27 +00:00
|
|
|
def load_search(self, area):
|
|
|
|
"""
|
|
|
|
Set `search` attribute.
|
|
|
|
Note that this method is irreversible.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
area:
|
|
|
|
"""
|
|
|
|
for b in self.iter_buttons():
|
|
|
|
b.search = area
|
|
|
|
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
class ClickButton:
|
2023-11-07 05:38:31 +00:00
|
|
|
def __init__(self, area, button=None, name='CLICK_BUTTON'):
|
|
|
|
self.area = area
|
|
|
|
if button is None:
|
|
|
|
self.button = area
|
|
|
|
else:
|
|
|
|
self.button = button
|
2023-05-16 18:54:03 +00:00
|
|
|
self.name = name
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
__repr__ = __str__
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return str(self) == str(other)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.name)
|
2023-05-14 07:48:34 +00:00
|
|
|
|
2023-05-16 18:54:03 +00:00
|
|
|
def __bool__(self):
|
|
|
|
return True
|
2023-05-30 18:20:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def match_template(image, template, similarity=0.85):
|
|
|
|
"""
|
|
|
|
Args:
|
|
|
|
image (np.ndarray): Screenshot
|
|
|
|
template (np.ndarray):
|
|
|
|
area (tuple): Crop area of image.
|
|
|
|
offset (int, tuple): Detection area offset.
|
|
|
|
similarity (float): 0-1. Similarity. Lower than this value will return float(0).
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool:
|
|
|
|
"""
|
|
|
|
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
|
|
|
|
_, sim, _, point = cv2.minMaxLoc(res)
|
|
|
|
return sim > similarity
|