LoginSignup
10
8

More than 3 years have passed since last update.

pyautoguiのlocateOnScreen()がマルチディスプレイ非対応な理由

Last updated at Posted at 2020-04-24

概要

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行目で以下のように定義されています。

payautogui/__init__.py
 def locateOnScreen(*args, **kwargs):
        return pyscreeze.locateOnScreen(*args, **kwargs)

つまりこの関数は別のモジュールの関数をそのまま引っ張ってきているだけでした。
そして、この pyscreeze.locateOnScreen()はpyscreeze/__init__.pyの350行目に以下のように定義されています。

pyscreeze/__init__.py
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行目にあり、

pyscreeze/__init__.py
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行目以下に定義されています。

PIL/ImageGrab.py
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のリポジトリで見てみると

PIL/ImageGrab.py
offset, size, data = Image.core.grabscreen_win32(
                include_layered_windows, all_screens
            )

となっていました。pillowって最新版7.0.0越えてるんですね...
grabscreen_win32_imaging.c

_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行目にありました。
また、このポストによるとこの関数内で使われているGetDeviceCapsがプライマリモニターのピクセルサイズしか返さないとのこと。調べていると、この関数はどうやらwindowsのAPIらしく、ドキュメントがありました。
display.cの337行目の

Pillow/display.c
width = GetDeviceCaps(screen, HORZRES);
height = GetDeviceCaps(screen, VERTRES);

がディスプレイのサイズを受け取っている所のようなので、もしかしてここをオラオラで変えてやればいけるのでは?というのは明日以降にします(今日はバイトがあるので)

と思ったもののDisplay.cが見つけられませんでした。なんで.....?
また進展があり次第追記・編集します。

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8