読者です 読者をやめる 読者になる 読者になる

yukiのブログ

作ったものなど

pythonでスクリーンショットを正しくとる(Windows)

問題

pythonの画像処理モジュールPillowにはスクリーンショットをとる関数PIL.Imagegrab.grab()があります。しかしこれはWindowsの一部の環境だとうまく動きません。具体的には、スクリーンショットをとっても左上の部分しか記録されません。

from PIL import ImageGrab
img = ImageGrab.grab()
img.save("screenshot.png")

上のコードを実行すると以下の画像が保存されます。 f:id:e-yuki67:20170212152645p:plain

この画像は本来保存されるべき画像とは異なっています。 f:id:e-yuki67:20170212152635p:plain

少し調べたところ、Windowsのスケーリング機能(デスクトップで右クリック->ディスプレイ設定で見れる)を使っているのが原因なことが分かりました。この機能は解像度が高いディスプレイでウィンドウや文字を大きく表示するために使われる機能なのですが、OSの内部ではディスプレイの解像度を小さくして座標の計算などが行われているらしく、この機能を使っているとディスプレイの解像度を取得する関数が正しい解像度を返してくれないようです。

その結果として、例えばスケーリング倍率を1.5倍にしていると解像度が1/1.5=2/3になったものとして扱われ、左上の2/3の領域しかキャプチャされません。

解決法

さらに調べると同じ問題とその解決法をPillowのgithubリポジトリissueで見つけました。そこには正しくスクリーンショットを取るコードも書いてあり、自分の環境でもうまくいきました。

issueに書いてあるコードはグローバル変数を使っているのであまりきれいなコードとは言えなかったのですが、自分で書いて自分で使うコードに使うだけだったのでそのまま使いました。ディスプレイの本当の解像度を取得する方法があるはずですが、見つけることができませんでした。

動いたコードを書いておきます。実行にはpywin32というモジュールが必要ですが、このモジュールはpipからインストールできないようなので注意が必要です。ググればインストーラーが出てきます。

import win32gui
import win32ui
import win32con
from PIL import Image

SCREEN_WIDTH = 2160
SCREEN_HEIGHT = 1440
SCREEN_SCALING_FACTOR = 1.5

def screenshot():
    """ スクリーンショット撮ってそれを(Pillow.Imageで)返す """
    window = win32gui.GetDesktopWindow()
    window_dc = win32ui.CreateDCFromHandle(win32gui.GetWindowDC(window))
    compatible_dc = window_dc.CreateCompatibleDC()
    width = SCREEN_WIDTH
    height = SCREEN_HEIGHT
    bmp = win32ui.CreateBitmap()
    bmp.CreateCompatibleBitmap(window_dc, width, height)
    compatible_dc.SelectObject(bmp)
    compatible_dc.BitBlt((0, 0), (width, height), window_dc, (0, 0), win32con.SRCCOPY)
    img = Image.frombuffer('RGB', (width, height), bmp.GetBitmapBits(True), 'raw', 'BGRX', 0, 1)
    return img

成果物

この関数を使って出来上がったのがこれです。同じサイズのスクリーンショットを複数とるのに適しています。

github.com