概要
pyautoguiのlocateOnScreen()がデュアルディスプレイ環境にも関わらず一画面分でしか探索ができなかったのでわかったところまでを備忘録として記します。
結論
PILのimageGrab.grab()がデュアルディスプレイに対応していない
pyautgiuのマルチモニター非対応の解決策を書いている人がいました(未検証)→https://github.com/python-pillow/Pillow/issues/1547#issuecomment-185815425
問題発生時の環境
Winodws10 home
python 3.6.4
pyautogui 0.9.50
pyscreeze 0.1.26
Pillow 5.0.0
pyautogui.locateOnScreen()の動作
この関数はpayautoguiの__init__.py
の212行目で以下のように定義されています。
def locateOnScreen(*args, **kwargs):
return pyscreeze.locateOnScreen(*args, **kwargs)
つまりこの関数は別のモジュールの関数をそのまま引っ張ってきているだけでした。
そして、この pyscreeze.locateOnScreen()はpyscreeze/__init__.py
の350行目に以下のように定義されています。
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
まずはじめにscreenshot(region=None)
でスクリーンショットを撮り、その中からretVal = locate(image, screenshotIm, **kwargs)
で与えられた画像との一致箇所を探しているようです。
ということは、このscreenshot関数が一画面分しか撮れていなければマルチディスプレイ全体で探し出すことはできません。
このscreenshot関数は同ファイルの591行目にあるようにプラットフォーム毎に関数が変わる仕組みになっていて、windowsではscreenshot = _screenshot_win32
となっています。これは同ファイル421行目にあり、
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
となっていて、im = ImageGrab.grab()でスクリーンショットを撮ってそれを返しています。つまり、pyautoguiのlocateOnScreen()がマルチディスプレイに対応していないのは、pillowのImageGran.grab()
がプライマリモニターしかスクショを撮れないところに起因しています。
調べているとちょうど同じ問題を解決した(しかもpyautogui!)方をみつけました。
https://github.com/python-pillow/Pillow/issues/1547#issuecomment-185815425
解決できそうなので、ついでに何故ImageGrab.grab()
がマルチディスプレイに対応していないかを見てみたいと思います。
この関数はPIL/ImageGrab.pyの32行目以下に定義されています。
def grab(bbox=None):
if sys.platform == "darwin":
fh, filepath = tempfile.mkstemp('.png')
os.close(fh)
subprocess.call(['screencapture', '-x', filepath])
im = Image.open(filepath)
im.load()
os.unlink(filepath)
else:
size, data = grabber()
im = Image.frombytes(
"RGB", size, data,
# RGB, 32-bit line padding, origin lower left corner
"raw", "BGR", (size[0]*3 + 3) & -4, -1
)
if bbox:
im = im.crop(bbox)
return im
このようになっていて、windowsの場合、スクリーンショットの大きさはgrabber()
によって決められていることがわかります。このgrabber関数は同ファイル24行目にあり
grabber = Image.core.grabscreen
となっています。
しかし、自分はこのImage.core.grabberが見つからず心がしんどくなりました。
ちなみにここをgithubのリポジトリで見てみると
offset, size, data = Image.core.grabscreen_win32(
include_layered_windows, all_screens
)
となっていました。pillowって最新版7.0.0越えてるんですね...
grabscreen_win32
は_imaging.cに
{"grabscreen_win32", (PyCFunction)PyImaging_GrabScreenWin32, 1},
とかいてあるのを見つけました。これはどうもC/Python APIでの書き方だそうですが、C言語が読めないためよくわからず...
.
.
.
と思いきやこんなポストを発見。
https://gitmemory.com/issue/python-pillow/Pillow/1547/499147024
これにはPyImaging_GrabScreenWin32()
はdisplay.c
にあるとあります。
さっそく見てみると[323行目](PyImaging_GrabScreenWin32(PyObject* self, PyObject* args))にありました。
また、このポストによるとこの関数内で使われているGetDeviceCaps
がプライマリモニターのピクセルサイズしか返さないとのこと。調べていると、この関数はどうやらwindowsのAPIらしく、ドキュメントがありました。
display.c
の337行目の
width = GetDeviceCaps(screen, HORZRES);
height = GetDeviceCaps(screen, VERTRES);
がディスプレイのサイズを受け取っている所のようなので、もしかしてここをオラオラで変えてやればいけるのでは?というのは明日以降にします(今日はバイトがあるので)
と思ったもののDisplay.cが見つけられませんでした。なんで.....?
また進展があり次第追記・編集します。