Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 17 additions & 49 deletions pikuli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,29 @@
# -*- coding: utf-8 -*-

''' Пока что этот модуль -- прослойка для Sikuli.
В перспективе мы сможем отказаться от Sikuli, дописывая только этот модуль

Doc pywin32:
http://timgolden.me.uk/pywin32-docs/modules.html

Особенности использования памяти:
-- При создании объекта Pattern, от сделает
self._cv2_pattern = cv2.imread(self.getFilename())

'''

#SUPPORT_UIA = True

import os
import sys

from .logger import logger
from .utils import wait_while, wait_while_not
from ._logger import logger
from ._settings_class import settings

from ._SettingsClass import SettingsClass
Settings = SettingsClass()

from ._exceptions import FailExit, FindFailed
from ._functions import * # TODO: remove it
from ._exceptions import PikuliError, FailExit, FindFailed

from .geom.vector import Vector, RelativeVec
from .geom.region import Region
from .geom.location import Location, LocationF

from .Screen import Screen
from .Match import Match
from .Pattern import Pattern

if os.name == 'nt':
from .hwnd.hwnd_element import HWNDElement

#if SUPPORT_UIA:
# from .uia import UIAElement # , AutomationElement
# from .uia.control_wrappers import RegisteredControlClasses
# RegisteredControlClasses._register_all()

__addImagePath_err_msg = "The directory of the ran py-file cann't be set as images path: {}"
try:
Settings.addImagePath(
os.path.dirname(os.path.abspath(sys.modules['__main__'].__file__)))
except Exception as e:
logger.exception(e)

__all__ = [
'Settings',
'Region',
'Screen',
'Match',
'Location',
'LocationF',
'Pattern',
'FailExit',
'FindFailed',
] # TODO: shorter this list
__main_module = sys.modules['__main__']
try:
__main_module_file = __main_module.__file__
except Exception as __ex1:
logger.warning(__addImagePath_err_msg.format(__ex1))
else:
settings.addImagePath(
os.path.dirname(os.path.abspath(__main_module_file)))
except Exception as __ex:
logger.exception(__addImagePath_err_msg.format(__ex))

from . import uia
from . import cv
4 changes: 0 additions & 4 deletions pikuli/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@

import traceback


class PikuliError(RuntimeError):
pass


class PostMoveCheck(PikuliError):
pass


class FailExit(PikuliError):
pass


class FindFailed(PikuliError):
""" This exception is raised when an image pattern
is not found on the screen.
Expand Down
83 changes: 39 additions & 44 deletions pikuli/_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,26 @@
'''
Файл содержит вспомогательные функции, используемые в pikuli.
'''
from distutils import extension
import os
import time
import logging
from typing import Any

import mss
from io import BytesIO
from PIL import Image

if os.name == 'nt':
import win32api
import win32gui
import win32con

import numpy as np

import pikuli
from ._exceptions import FailExit, FindFailed
from pikuli import logger

from .geom.simple_types import Rectangle
from . import FailExit, logger, settings

# Константа отсутствует в win32con, но есть в http://userpages.umbc.edu/~squire/download/WinGDI.h:
CAPTUREBLT = 0x40000000


def verify_timeout_argument(timeout, allow_None=False, err_msg='pikuli.verify_timeout_argument()'):
if timeout is None and allow_None:
return None
Expand All @@ -37,9 +34,8 @@ def verify_timeout_argument(timeout, allow_None=False, err_msg='pikuli.verify_ti
raise FailExit('%s: wrong timeout = \'%s\' (%s)' % (str(err_msg), str(timeout), str(ex)))
return timeout


def addImagePath(path):
pikuli.Settings.addImagePath(path)
settings.addImagePath(path)

def get_hwnd_by_location(x, y):
'''
Expand All @@ -48,7 +44,7 @@ def get_hwnd_by_location(x, y):
return win32gui.WindowFromPoint((x, y))

def setFindFailedDir(path):
pikuli.Settings.setFindFailedDir(path)
settings.setFindFailedDir(path)

def _monitor_hndl_to_screen_n(m_hndl):
''' Экраны-мониторы нуменруются от 1. Нулевой экран -- это полный вирутальный. '''
Expand All @@ -58,12 +54,10 @@ def _monitor_hndl_to_screen_n(m_hndl):
raise FailExit('can not obtaen Screen number from win32api.GetMonitorInfo() = %s' % str(minfo))
return screen_n


def _screen_n_to_monitor_name(n):
''' Экраны-мониторы нуменруются от 1. Нулевой экран -- это полный вирутальный. '''
return r'\\.\DISPLAY%i' % n


def _screen_n_to_mon_descript(n):
''' Returns a sequence of tuples. For each monitor found, returns a handle to the monitor, device context handle, and intersection rectangle:
(hMonitor, hdcMonitor, PyRECT) '''
Expand All @@ -81,7 +75,6 @@ def _screen_n_to_mon_descript(n):
raise FailExit('wrong screen number \'%s\'' % str(n))
return m


def highlight_region(x, y, w, h, delay=0.5):
def _cp_boundary(dest_dc, dest_x0, dest_y0, src_dc, src_x0, src_y0, w, h):
win32gui.BitBlt(dest_dc, dest_x0+0, dest_y0+0, w, 1, src_dc, src_x0, src_y0, win32con.SRCCOPY)
Expand Down Expand Up @@ -126,52 +119,54 @@ def _thread_function(x, y, w, h, delay):
#threading.Thread(target=_thread_function, args=(x, y, w, h, delay), name='highlight_region %s' % str((x, y, w, h, delay))).start()
return

class SimpleImage:

def _take_screenshot(x, y, w, h, hwnd=None):
def __init__(self, img_src: Any, pil_img: Image):
self.img_src = img_src
self._pil_img = pil_img

@classmethod
def from_cv2(cls, img_src: Any, cv2_img):
return Image.fromarray(cv2_img, mode="RGB")

@property
def pillow_img(self) -> Image:
return self._pil_img

def save(self, filename, loglevel=logging.DEBUG):
path = os.path.abspath(filename)
logger.log(loglevel, f'Save {self.img_src} to {path}')

dir_path = os.path.dirname(path)
if not os.path.exists(dir_path):
os.makedirs(dir_path)

_, extension = os.path.splitext(path)
self._pil_img.save(path, format=extension.lstrip('.'))

def take_screenshot(rect: Rectangle) -> SimpleImage:
'''
Получаем скриншот области:
x, y -- верхний левый угол прямоуголника в системе координат виртуального рабочего стола
w, h -- размеры прямоуголника
#

# TODO: Fix multi-monitor configuration!!!
'''
with mss.mss() as sct:
monitor = sct.monitors[0]
max_x = monitor["width"]
max_y = monitor["height"]
# проверка выхода заданного значения width за допустимый диапозон
w = w if x + w < max_x else max_x - x
w = rect.w if rect.x + rect.w < max_x else max_x - rect.x
# проверка выхода заданного значения height за допустимый диапозон
h = h if y + h < max_y else max_y - y
sct_img = sct.grab(dict(left=x, top=y, height=h, width=w))
scr = mss.tools.to_png(sct_img.rgb, sct_img.size, output="")
return np.array(Image.open(BytesIO(scr)).convert('RGB'))


"""def _scr_num_of_point(x, y):
''' Вернет номер (от нуля) того экрана, на котором располоржен левый верхний угол текущего Region. '''
m_tl = win32api.MonitorFromPoint((x, y), win32con.MONITOR_DEFAULTTONULL)
if m_tl is None:
raise FailExit('top-left corner of the Region is out of visible area of sreens (%s, %s)' % (str(x), str(y)))
return _monitor_hndl_to_screen_n(m_tl)"""

"""
def __check_reg_in_single_screen(self):
''' Проверяем, что Region целиком на одном экране. Экран -- это просто один из мониторав, которые существуют по мнению Windows. '''
m_tl = win32api.MonitorFromPoint((self._x, self._y), win32con.MONITOR_DEFAULTTONULL)
# Do "-1" to get the edge pixel belonget to the Region. The next pixel (over any direction) is out of the Region:
m_br = win32api.MonitorFromPoint((self._x + self._w - 1, self._y + self._h - 1), win32con.MONITOR_DEFAULTTONULL)
if m_tl is None or m_br is None:
raise FailExit('one or more corners of region out of visible area of sreens')
if m_tl != m_br:
raise FailExit('region occupies more than one screen')
return Screen(_monitor_hndl_to_screen_n(m_tl))
"""

h = rect.h if rect.y + rect.h < max_y else max_y - rect.y
sct_img = sct.grab(dict(left=rect.x, top=rect.y, height=h, width=w))
pil_img = Image.frombytes("RGB", sct_img.size, sct_img.bgra, "raw", "BGRX")
return SimpleImage(rect, pil_img)

def pixel_color_at(x, y, monitor_number=1):
return pixels_colors_at([(x, y)], monitor_number)[0]


def pixels_colors_at(coords_tuple_list, monitor_number=1):
with mss.mss() as sct:
sct_img = sct.grab(sct.monitors[monitor_number]) # некст по умолчанию выводится на первый монитор
Expand Down
4 changes: 1 addition & 3 deletions pikuli/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@

import os

from pikuli import logger

from . import logger

class NotImplemetedDummyBase(object):
err_msg = None


class NotImplemetedDummyFactory(object):

class _AttrPlaceholder(object):
Expand Down
6 changes: 3 additions & 3 deletions pikuli/logger.py → pikuli/_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
import logging

def basic_logger_config(loglevel=logging.INFO):
def basic_logger_config(logger, loglevel=logging.INFO):
if logger.handlers:
logger.info('Pikuli logger already configured. Skip `pikuli.utils.basic_logger_config()`.')
return
Expand All @@ -19,6 +19,6 @@ def basic_logger_config(loglevel=logging.INFO):
logger.addHandler(handler)
logger.debug('Pikuli logger has been configured basicaly')


logger = logging.getLogger('axxon.pikuli')
basic_logger_config()

basic_logger_config(logger)
5 changes: 3 additions & 2 deletions pikuli/_SettingsClass.py → pikuli/_settings_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import os
import tempfile


class SettingsClass(object):
class SettingsClass:

__def_IMG_ADDITION_PATH = [] # Пути, кроме текущего и мб еще какого-то подобного
__def_MinSimilarity = 0.995 # Почти устойчиво с 0.995, но однажны не нашел узелок для контура. 0.700 -- будет найдено в каждом пикселе (порог надо поднимать выше).
Expand Down Expand Up @@ -52,3 +51,5 @@ def setPatternURLTemplate(self, GetPattern_URLTemplate):

def getPatternURLTemplate(self):
return self.GetPattern_URLTemplate

settings = SettingsClass()
10 changes: 10 additions & 0 deletions pikuli/cv/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-

from .region_cv_methods import RegionCVMethods
from ..geom import Region as __Region
__Region._make_cv_methods_class_instance = RegionCVMethods

from .file import File
from .match import Match
from .pattern import Pattern
from .screen import Screen
3 changes: 1 addition & 2 deletions pikuli/File.py → pikuli/cv/file.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-


import os

class File(object):
class File:
def __init__(self, path):
if path is None:
self._path = None
Expand Down
32 changes: 16 additions & 16 deletions pikuli/Match.py → pikuli/cv/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
'''

import traceback
from . import FailExit
from . import Region

from .. import FailExit
from ..geom import Region

class Match(Region):
class Match:
'''
Как потомок Region, класс Match сможет хранить в себе картинку в формате numpy.array; сравнивать
сохраненную картинку с тем, что сейчас в области (x, y, w, h) отображается на экране. Будем по
Expand All @@ -24,31 +24,31 @@ def __init__(self, x, y, w, h, pattern, score):
score -- число, показывающее достоверность совпадения шаблона с изображение на экране
'''
try:
super(Match, self).__init__(x, y, w, h)
if not( score is None or (isinstance(score, float) and score > 0.0 and score <= 1.0) ):
if not( (score is None) or (isinstance(score, float) and score > 0.0 and score <= 1.0) ):
raise FailExit('not( score is None or (isinstance(score, float) and score > 0.0 and score <= 1.0) ):')
self._region = Region(x, y, w, h)
self._img = self._region.cv.take_screenshot()
self._score = score
self._pattern = pattern
self.store_current_image()

except FailExit:
raise FailExit('\nNew stage of %s\n[error] Incorect \'Match\' constructor call:\n\tx = %s\n\ty = %s\n\tw = %s\n\th = %s\n\tscore = %s\n\t' % (traceback.format_exc(), str(w), str(y), str(w), str(h), str(score)))

def __str__(self):
return ('<Match of \'%s\' in (%i, %i, %i, %i) with score = %.3f>' % (str(self._pattern.getFilename()), self._x, self._y, self._w, self._h, self._score))
return repr(self)

def __repr__(self):
return ('<Match of \'%s\' in (%i, %i, %i, %i) with score = %.3f>' % (str(self._pattern.getFilename()), self._x, self._y, self._w, self._h, self._score))
return f'<Match of \'{self._pattern.filename}\' in {self._region} with score = {self._score:.3f}>'

def getScore(self):
@property
def region(self):
return self._region

@property
def score(self):
''' Sikuli: Get the similarity score the image or pattern was found. The value is between 0 and 1. '''
return self._score

def getTarget(self):
''' Sikuli: Get the 'location' object that will be used as the click point.
Typically, when no offset was specified by Pattern.targetOffset(), the click point is the center of the matched region.
If an offset was given, the click point is the offset relative to the center. '''
raise NotImplementedError

def getPattern(self):
@property
def pattern(self):
return self._pattern
Loading