Pracownia_programowania/venv/Lib/site-packages/pyscreeze/__init__.py
2020-02-01 20:05:44 +01:00

611 lines
23 KiB
Python

# PyScreeze
"""
NOTE:
Apparently Pillow support on Ubuntu 64-bit has several additional steps since it doesn't have JPEG/PNG support out of the box. Description here:
https://stackoverflow.com/questions/7648200/pip-install-pil-e-tickets-1-no-jpeg-png-support
http://ubuntuforums.org/showthread.php?t=1751455
"""
__version__ = '0.1.26'
import collections
import datetime
import functools
import os
import subprocess
import sys
import time
import errno
from contextlib import contextmanager
try:
from PIL import Image
from PIL import ImageOps
from PIL import ImageDraw
if sys.platform == 'win32': # TODO - Pillow now supports ImageGrab on macOS.
from PIL import ImageGrab
_PILLOW_UNAVAILABLE = False
except ImportError:
# We ignore this because failures due to Pillow not being installed
# should only happen when the functions that specifically depend on
# Pillow are called. The main use case is when PyAutoGUI imports
# PyScreeze, but Pillow isn't installed because the user is running
# some platform/version of Python that Pillow doesn't support, then
# importing PyAutoGUI should not automatically fail because it
# imports PyScreeze.
# So we have a `pass` statement here since a failure to import
# Pillow shouldn't crash PyScreeze.
_PILLOW_UNAVAILABLE = True
try:
import cv2, numpy
useOpenCV = True
RUNNING_CV_2 = cv2.__version__[0] < '3'
except ImportError:
useOpenCV = False
RUNNING_PYTHON_2 = sys.version_info[0] == 2
if useOpenCV:
if RUNNING_CV_2:
LOAD_COLOR = cv2.CV_LOAD_IMAGE_COLOR
LOAD_GRAYSCALE = cv2.CV_LOAD_IMAGE_GRAYSCALE
else:
LOAD_COLOR = cv2.IMREAD_COLOR
LOAD_GRAYSCALE = cv2.IMREAD_GRAYSCALE
if not RUNNING_PYTHON_2:
unicode = str # On Python 3, all the isinstance(spam, (str, unicode)) calls will work the same as Python 2.
if sys.platform == 'win32':
# On Windows, the monitor scaling can be set to something besides normal 100%.
# PyScreeze and Pillow needs to account for this to make accurate screenshots.
# TODO - How does macOS and Linux handle monitor scaling?
import ctypes
try:
ctypes.windll.user32.SetProcessDPIAware()
except AttributeError:
pass # Windows XP doesn't support monitor scaling, so just do nothing.
GRAYSCALE_DEFAULT = False
# For version 0.1.19 I changed it so that ImageNotFoundException was raised
# instead of returning None. In hindsight, this change came too late, so I'm
# changing it back to returning None. But I'm also including this option for
# folks who would rather have it raise an exception.
USE_IMAGE_NOT_FOUND_EXCEPTION = False
scrotExists = False
try:
if sys.platform not in ('java', 'darwin', 'win32'):
whichProc = subprocess.Popen(
['which', 'scrot'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
scrotExists = whichProc.wait() == 0
except OSError as ex:
if ex.errno == errno.ENOENT:
# if there is no "which" program to find scrot, then assume there
# is no scrot.
pass
else:
raise
if sys.platform == 'win32':
from ctypes import windll
# win32 DC(DeviceContext) Manager
@contextmanager
def __win32_openDC(hWnd):
"""
TODO
"""
hDC = windll.user32.GetDC(hWnd)
if hDC == 0: #NULL
raise WindowsError("windll.user32.GetDC failed : return NULL")
try:
yield hDC
finally:
if windll.user32.ReleaseDC(hWnd, hDC) == 0:
raise WindowsError("windll.user32.ReleaseDC failed : return 0")
Box = collections.namedtuple('Box', 'left top width height')
Point = collections.namedtuple('Point', 'x y')
RGB = collections.namedtuple('RGB', 'red green blue')
class PyScreezeException(Exception):
pass # This is a generic exception class raised when a PyScreeze-related error happens.
class ImageNotFoundException(PyScreezeException):
pass # This is an exception class raised when the locate functions fail to locate an image.
def requiresPillow(wrappedFunction):
"""
A decorator that marks a function as requiring Pillow to be installed.
This raises PyScreezeException if Pillow wasn't imported.
"""
@functools.wraps(wrappedFunction)
def wrapper(*args, **kwargs):
if _PILLOW_UNAVAILABLE:
raise PyScreezeException('The Pillow package is required to use this function.')
return wrappedFunction(*args, **kwargs)
return wrapper
def _load_cv2(img, grayscale=None):
"""
TODO
"""
# load images if given filename, or convert as needed to opencv
# Alpha layer just causes failures at this point, so flatten to RGB.
# RGBA: load with -1 * cv2.CV_LOAD_IMAGE_COLOR to preserve alpha
# to matchTemplate, need template and image to be the same wrt having alpha
if grayscale is None:
grayscale = GRAYSCALE_DEFAULT
if isinstance(img, (str, unicode)):
# The function imread loads an image from the specified file and
# returns it. If the image cannot be read (because of missing
# file, improper permissions, unsupported or invalid format),
# the function returns an empty matrix
# http://docs.opencv.org/3.0-beta/modules/imgcodecs/doc/reading_and_writing_images.html
if grayscale:
img_cv = cv2.imread(img, LOAD_GRAYSCALE)
else:
img_cv = cv2.imread(img, LOAD_COLOR)
if img_cv is None:
raise IOError("Failed to read %s because file is missing, "
"has improper permissions, or is an "
"unsupported or invalid format" % img)
elif isinstance(img, numpy.ndarray):
# don't try to convert an already-gray image to gray
if grayscale and len(img.shape) == 3: # and img.shape[2] == 3:
img_cv = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
else:
img_cv = img
elif hasattr(img, 'convert'):
# assume its a PIL.Image, convert to cv format
img_array = numpy.array(img.convert('RGB'))
img_cv = img_array[:, :, ::-1].copy() # -1 does RGB -> BGR
if grayscale:
img_cv = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
else:
raise TypeError('expected an image filename, OpenCV numpy array, or PIL image')
return img_cv
def _locateAll_opencv(needleImage, haystackImage, grayscale=None, limit=10000, region=None, step=1,
confidence=0.999):
"""
TODO - rewrite this
faster but more memory-intensive than pure python
step 2 skips every other row and column = ~3x faster but prone to miss;
to compensate, the algorithm automatically reduces the confidence
threshold by 5% (which helps but will not avoid all misses).
limitations:
- OpenCV 3.x & python 3.x not tested
- RGBA images are treated as RBG (ignores alpha channel)
"""
if grayscale is None:
grayscale = GRAYSCALE_DEFAULT
confidence = float(confidence)
needleImage = _load_cv2(needleImage, grayscale)
needleHeight, needleWidth = needleImage.shape[:2]
haystackImage = _load_cv2(haystackImage, grayscale)
if region:
haystackImage = haystackImage[region[1]:region[1]+region[3],
region[0]:region[0]+region[2]]
else:
region = (0, 0) # full image; these values used in the yield statement
if (haystackImage.shape[0] < needleImage.shape[0] or
haystackImage.shape[1] < needleImage.shape[1]):
# avoid semi-cryptic OpenCV error below if bad size
raise ValueError('needle dimension(s) exceed the haystack image or region dimensions')
if step == 2:
confidence *= 0.95
needleImage = needleImage[::step, ::step]
haystackImage = haystackImage[::step, ::step]
else:
step = 1
# get all matches at once, credit: https://stackoverflow.com/questions/7670112/finding-a-subimage-inside-a-numpy-image/9253805#9253805
result = cv2.matchTemplate(haystackImage, needleImage, cv2.TM_CCOEFF_NORMED)
match_indices = numpy.arange(result.size)[(result > confidence).flatten()]
matches = numpy.unravel_index(match_indices[:limit], result.shape)
if len(matches[0]) == 0:
if USE_IMAGE_NOT_FOUND_EXCEPTION:
raise ImageNotFoundException('Could not locate the image (highest confidence = %.3f)' % result.max())
else:
return
# use a generator for API consistency:
matchx = matches[1] * step + region[0] # vectorized
matchy = matches[0] * step + region[1]
for x, y in zip(matchx, matchy):
yield Box(x, y, needleWidth, needleHeight)
# TODO - We should consider renaming _locateAll_python to _locateAll_pillow, since Pillow is the real dependency.
@requiresPillow
def _locateAll_python(needleImage, haystackImage, grayscale=None, limit=None, region=None, step=1):
"""
TODO
"""
# setup all the arguments
if grayscale is None:
grayscale = GRAYSCALE_DEFAULT
needleFileObj = None
if isinstance(needleImage, (str, unicode)):
# 'image' is a filename, load the Image object
needleFileObj = open(needleImage, 'rb')
needleImage = Image.open(needleFileObj)
haystackFileObj = None
if isinstance(haystackImage, (str, unicode)):
# 'image' is a filename, load the Image object
haystackFileObj = open(haystackImage, 'rb')
haystackImage = Image.open(haystackFileObj)
if region is not None:
haystackImage = haystackImage.crop((region[0], region[1], region[0] + region[2], region[1] + region[3]))
else:
region = (0, 0) # set to 0 because the code always accounts for a region
if grayscale: # if grayscale mode is on, convert the needle and haystack images to grayscale
needleImage = ImageOps.grayscale(needleImage)
haystackImage = ImageOps.grayscale(haystackImage)
else:
# if not using grayscale, make sure we are comparing RGB images, not RGBA images.
if needleImage.mode == 'RGBA':
needleImage = needleImage.convert('RGB')
if haystackImage.mode == 'RGBA':
haystackImage = haystackImage.convert('RGB')
# setup some constants we'll be using in this function
needleWidth, needleHeight = needleImage.size
haystackWidth, haystackHeight = haystackImage.size
needleImageData = tuple(needleImage.getdata())
haystackImageData = tuple(haystackImage.getdata())
needleImageRows = [needleImageData[y * needleWidth:(y+1) * needleWidth] for y in range(needleHeight)] # LEFT OFF - check this
needleImageFirstRow = needleImageRows[0]
assert len(needleImageFirstRow) == needleWidth, 'For some reason, the calculated width of first row of the needle image is not the same as the width of the image.'
assert [len(row) for row in needleImageRows] == [needleWidth] * needleHeight, 'For some reason, the needleImageRows aren\'t the same size as the original image.'
numMatchesFound = 0
# NOTE: After running tests/benchmarks.py on the following code, it seem that having a step
# value greater than 1 does not give *any* significant performance improvements.
# Since using a step higher than 1 makes for less accurate matches, it will be
# set to 1.
step = 1 # hard-code step as 1 until a way to improve it can be figured out.
if step == 1:
firstFindFunc = _kmp
else:
firstFindFunc = _steppingFind
for y in range(haystackHeight): # start at the leftmost column
for matchx in firstFindFunc(needleImageFirstRow, haystackImageData[y * haystackWidth:(y+1) * haystackWidth], step):
foundMatch = True
for searchy in range(1, needleHeight, step):
haystackStart = (searchy + y) * haystackWidth + matchx
if needleImageData[searchy * needleWidth:(searchy+1) * needleWidth] != haystackImageData[haystackStart:haystackStart + needleWidth]:
foundMatch = False
break
if foundMatch:
# Match found, report the x, y, width, height of where the matching region is in haystack.
numMatchesFound += 1
yield Box(matchx + region[0], y + region[1], needleWidth, needleHeight)
if limit is not None and numMatchesFound >= limit:
# Limit has been reached. Close file handles.
if needleFileObj is not None:
needleFileObj.close()
if haystackFileObj is not None:
haystackFileObj.close()
return
# There was no limit or the limit wasn't reached, but close the file handles anyway.
if needleFileObj is not None:
needleFileObj.close()
if haystackFileObj is not None:
haystackFileObj.close()
if numMatchesFound == 0:
if USE_IMAGE_NOT_FOUND_EXCEPTION:
raise ImageNotFoundException('Could not locate the image.')
else:
return
def locate(needleImage, haystackImage, **kwargs):
"""
TODO
"""
# Note: The gymnastics in this function is because we want to make sure to exhaust the iterator so that the needle and haystack files are closed in locateAll.
kwargs['limit'] = 1
points = tuple(locateAll(needleImage, haystackImage, **kwargs))
if len(points) > 0:
return points[0]
else:
if USE_IMAGE_NOT_FOUND_EXCEPTION:
raise ImageNotFoundException('Could not locate the image.')
else:
return None
def locateOnScreen(image, minSearchTime=0, **kwargs):
"""TODO - rewrite this
minSearchTime - amount of time in seconds to repeat taking
screenshots and trying to locate a match. The default of 0 performs
a single search.
"""
start = time.time()
while True:
try:
screenshotIm = screenshot(region=None) # the locateAll() function must handle cropping to return accurate coordinates, so don't pass a region here.
retVal = locate(image, screenshotIm, **kwargs)
try:
screenshotIm.fp.close()
except AttributeError:
# Screenshots on Windows won't have an fp since they came from
# ImageGrab, not a file. Screenshots on Linux will have fp set
# to None since the file has been unlinked
pass
if retVal or time.time() - start > minSearchTime:
return retVal
except ImageNotFoundException:
if time.time() - start > minSearchTime:
if USE_IMAGE_NOT_FOUND_EXCEPTION:
raise
else:
return None
def locateAllOnScreen(image, **kwargs):
"""
TODO
"""
# TODO - Should this raise an exception if zero instances of the image can be found on the screen, instead of always returning a generator?
screenshotIm = screenshot(region=None) # the locateAll() function must handle cropping to return accurate coordinates, so don't pass a region here.
retVal = locateAll(image, screenshotIm, **kwargs)
try:
screenshotIm.fp.close()
except AttributeError:
# Screenshots on Windows won't have an fp since they came from
# ImageGrab, not a file. Screenshots on Linux will have fp set
# to None since the file has been unlinked
pass
return retVal
def locateCenterOnScreen(image, **kwargs):
"""
TODO
"""
coords = locateOnScreen(image, **kwargs)
if coords is None:
return None
else:
return center(coords)
@requiresPillow
def showRegionOnScreen(region, outlineColor='red', filename='_showRegionOnScreen.png'):
"""
TODO
"""
# TODO - This function is useful! Document it!
screenshotIm = screenshot()
draw = ImageDraw.Draw(screenshotIm)
region = (region[0], region[1], region[2] + region[0], region[3] + region[1]) # convert from (left, top, right, bottom) to (left, top, width, height)
draw.rectangle(region, outline=outlineColor)
screenshotIm.save(filename)
@requiresPillow
def _screenshot_win32(imageFilename=None, region=None):
"""
TODO
"""
# TODO - Use the winapi to get a screenshot, and compare performance with ImageGrab.grab()
# https://stackoverflow.com/a/3586280/1893164
im = ImageGrab.grab()
if region is not None:
assert len(region) == 4, 'region argument must be a tuple of four ints'
region = [int(x) for x in region]
im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1]))
if imageFilename is not None:
im.save(imageFilename)
return im
def _screenshot_osx(imageFilename=None, region=None):
"""
TODO
"""
# TODO - use tmp name for this file.
if imageFilename is None:
tmpFilename = 'screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f'))
else:
tmpFilename = imageFilename
subprocess.call(['screencapture', '-x', tmpFilename])
im = Image.open(tmpFilename)
if region is not None:
assert len(region) == 4, 'region argument must be a tuple of four ints'
region = [int(x) for x in region]
im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1]))
os.unlink(tmpFilename) # delete image of entire screen to save cropped version
im.save(tmpFilename)
else:
# force loading before unlinking, Image.open() is lazy
im.load()
if imageFilename is None:
os.unlink(tmpFilename)
return im
def _screenshot_linux(imageFilename=None, region=None):
"""
TODO
"""
if not scrotExists:
raise NotImplementedError('"scrot" must be installed to use screenshot functions in Linux. Run: sudo apt-get install scrot')
if imageFilename is None:
tmpFilename = '.screenshot%s.png' % (datetime.datetime.now().strftime('%Y-%m%d_%H-%M-%S-%f'))
else:
tmpFilename = imageFilename
if scrotExists:
subprocess.call(['scrot', '-z', tmpFilename])
im = Image.open(tmpFilename)
if region is not None:
assert len(region) == 4, 'region argument must be a tuple of four ints'
region = [int(x) for x in region]
im = im.crop((region[0], region[1], region[2] + region[0], region[3] + region[1]))
os.unlink(tmpFilename) # delete image of entire screen to save cropped version
im.save(tmpFilename)
else:
# force loading before unlinking, Image.open() is lazy
im.load()
if imageFilename is None:
os.unlink(tmpFilename)
return im
else:
raise Exception('The scrot program must be installed to take a screenshot with PyScreeze on Linux. Run: sudo apt-get install scrot')
def _kmp(needle, haystack, _dummy): # Knuth-Morris-Pratt search algorithm implementation (to be used by screen capture)
"""
TODO
"""
# build table of shift amounts
shifts = [1] * (len(needle) + 1)
shift = 1
for pos in range(len(needle)):
while shift <= pos and needle[pos] != needle[pos-shift]:
shift += shifts[pos-shift]
shifts[pos+1] = shift
# do the actual search
startPos = 0
matchLen = 0
for c in haystack:
while matchLen == len(needle) or \
matchLen >= 0 and needle[matchLen] != c:
startPos += shifts[matchLen]
matchLen -= shifts[matchLen]
matchLen += 1
if matchLen == len(needle):
yield startPos
def _steppingFind(needle, haystack, step):
"""
TODO
"""
for startPos in range(0, len(haystack) - len(needle) + 1):
foundMatch = True
for pos in range(0, len(needle), step):
if haystack[startPos + pos] != needle[pos]:
foundMatch = False
break
if foundMatch:
yield startPos
def center(coords):
"""
Returns a `Point` object with the x and y set to an integer determined by the format of `coords`.
The `coords` argument is a 4-integer tuple of (left, top, width, height).
For example:
>>> center((10, 10, 6, 8))
Point(x=13, y=14)
>>> center((10, 10, 7, 9))
Point(x=13, y=14)
>>> center((10, 10, 8, 10))
Point(x=14, y=15)
"""
# TODO - one day, add code to handle a Box namedtuple.
return Point(coords[0] + int(coords[2] / 2), coords[1] + int(coords[3] / 2))
def pixelMatchesColor(x, y, expectedRGBColor, tolerance=0):
"""
TODO
"""
pix = pixel(x, y)
if len(pix) == 3 or len(expectedRGBColor) == 3: #RGB mode
r, g, b = pix[:3]
exR, exG, exB = expectedRGBColor[:3]
return (abs(r - exR) <= tolerance) and (abs(g - exG) <= tolerance) and (abs(b - exB) <= tolerance)
elif len(pix) == 4 and len(expectedRGBColor) == 4: #RGBA mode
r, g, b, a = pix
exR, exG, exB, exA = expectedRGBColor
return (abs(r - exR) <= tolerance) and (abs(g - exG) <= tolerance) and (abs(b - exB) <= tolerance) and (abs(a - exA) <= tolerance)
else:
assert False, 'Color mode was expected to be length 3 (RGB) or 4 (RGBA), but pixel is length %s and expectedRGBColor is length %s' % (len(pix), len(expectedRGBColor))
def pixel(x, y):
"""
TODO
"""
if sys.platform == 'win32':
# On Windows, calling GetDC() and GetPixel() is twice as fast as using our screenshot() function.
with __win32_openDC(0) as hdc: # handle will be released automatically
color = windll.gdi32.GetPixel(hdc, x, y)
if color < 0:
raise WindowsError("windll.gdi32.GetPixel failed : return {}".format(color))
# color is in the format 0xbbggrr https://msdn.microsoft.com/en-us/library/windows/desktop/dd183449(v=vs.85).aspx
bbggrr = "{:0>6x}".format(color) # bbggrr => 'bbggrr' (hex)
b, g, r = (int(bbggrr[i:i+2], 16) for i in range(0, 6, 2))
return (r, g, b)
else:
# Need to select only the first three values of the color in
# case the returned pixel has an alpha channel
return RGB(*(screenshot().getpixel((x, y))[:3]))
# set the screenshot() function based on the platform running this module
if sys.platform.startswith('java'):
raise NotImplementedError('Jython is not yet supported by PyScreeze.')
elif sys.platform == 'darwin':
screenshot = _screenshot_osx
elif sys.platform == 'win32':
screenshot = _screenshot_win32
else: # TODO - Make this more specific. "Anything else" does not necessarily mean "Linux".
screenshot = _screenshot_linux
grab = screenshot # for compatibility with Pillow/PIL's ImageGrab module.
# set the locateAll function to use opencv if possible; python 3 needs opencv 3.0+
# TODO - Should this raise an exception if zero instances of the image can be found on the screen, instead of always returning a generator?
if useOpenCV:
locateAll = _locateAll_opencv
if not RUNNING_PYTHON_2 and cv2.__version__ < '3':
locateAll = _locateAll_python
else:
locateAll = _locateAll_python