作ったプログラムの備忘録
はじめに
- 指定秒数毎に指定した範囲のスクリーンショットを撮って、画面に変化があった場合に保存するプログラム
- 動画でキャプチャするほどでもないけど、何度もスクリーンショットボタンを押すのは面倒だったので作成
動作テスト環境
OS: Windows 10 Pro 64bit
言語: Python 3.9.13
ライブラリ:OpenCV(opencv-contrib-python) 4.5.5.64
ライブラリ:Pillow 9.1.0
ライブラリ:numpy 1.21.6
ソースコード
import os
import cv2
import time
import ctypes
from ctypes import windll, wintypes
import numpy as np
from PIL import ImageGrab
from datetime import datetime as dt
def screenshot_position():
try:
windll.shcore.SetProcessDpiAwareness(True)
except:
pass
def position_get():
point = wintypes.POINT()
windll.user32.GetCursorPos(ctypes.byref(point))
return point.x, point.y
input('取得したい箇所の"左上"にカーソルを当てEnterキー押してください')
x1, y1 = position_get()
print(f'X:{str(x1)}, Y:{str(y1)}')
input('取得したい箇所の"右下"にカーソルを当てEnterキー押してください')
x2, y2 = position_get()
print(f'X:{str(x2)}, Y:{str(y2)}')
with open('screenshot_position.txt', 'w') as f:
f.write(f'{str(x1)}, {str(y1)}, {str(x2)}, {str(y2)}')
def screenshot_loop(stop_time, dir_path, x1, y1, x2, y2):
try:
while True:
time.sleep(stop_time)
img = ImageGrab.grab(bbox=(x1, y1, x2, y2), all_screens=True)
img_ima = np.array(img)
if 'img_mae' in locals():
backSub = cv2.createBackgroundSubtractorMOG2()
fgmask = backSub.apply(img_mae)
fgmask = backSub.apply(img_ima)
if np.count_nonzero(fgmask) / fgmask.size * 100 < 0.5:
continue
img.save(f'{dir_path}/{dt.now().strftime("%y%m%d%H%M%S")}.png')
img_mae = img_ima
except KeyboardInterrupt:
print('\n■■キャプチャ終了■■')
def main():
if not os.path.exists('screenshot_position.txt'):
print('スクリーンショット位置を設定してください')
screenshot_position()
else:
screenshot_set = input("スクリーンショットする位置を設定しますか?[y/any]")
if screenshot_set == "y" or screenshot_set == "Y":
screenshot_position()
x1, y1, x2, y2 = np.loadtxt('screenshot_position.txt', delimiter=',')
stop_time = input('撮影間隔(秒)を入力して、ENTERで開始(デフォルト値:2秒、最低値:1秒):')
stop_time = 2 if not stop_time else 1 if float(stop_time) < 1 else float(stop_time)
dir_path = dt.now().strftime('%y%m%d_%H%M')
if not os.path.exists(dir_path):
os.mkdir(dir_path)
print('\nスクリーンショットを開始しました\nCtl+cで終了できます')
screenshot_loop(stop_time, dir_path, x1, y1, x2, y2)
if __name__ == '__main__':
main()
解説
モジュール
import os
import cv2
import time
import ctypes
import numpy as np
from PIL import ImageGrab
from datetime import datetime as dt
-
numpy
:
画像の背景差分を取得する際にnumpy形式に変換する必要がある。インストールはpip install numpy
-
ImageGrab
:
スクリーンショットを取得するモジュール。インストールはpip install Pillow
-
cv2
:
OpenCV。画面変化を検出するために使用。createBackgroundSubtractorMOG2()
を使用するため、インストールはpip install opencv-contrib-python
とする必要がある - あとは標準モジュール
指定秒数間隔でスクリーンショットを取って、画面変化があれば保存する関数
def screenshot_loop(stop_time, dir_path, x1, y1, x2, y2):
try:
while True:
time.sleep(stop_time)
img = ImageGrab.grab(bbox=(x1, y1, x2, y2), all_screens=True)
img_ima = np.array(img)
if 'img_mae' in locals():
backSub = cv2.createBackgroundSubtractorMOG2()
fgmask = backSub.apply(img_mae)
fgmask = backSub.apply(img_ima)
if np.count_nonzero(fgmask) / fgmask.size * 100 < 0.5:
continue
img.save(f'{dir_path}/{dt.now().strftime("%y%m%d%H%M%S")}.png')
img_mae = img_ima
except KeyboardInterrupt:
print('\n■■キャプチャ終了■■')
-
メインとなる機能。自動でスクリーンショットして、画面変化があれば保存する関数
-
引数
-
stop_time
: スクリーンショット間隔(秒) -
dir_path
: 保存するディレクトリ名 -
x1, y1, x2, y2
: スクリーンショットを取得する画面の左上座標(x1, y1)と右下座標(x2, y2)
-
-
ImageGrab.grab
:
スクリーンショットを撮影。引数all_screens=True
とすることでデュアルディスプレイ環境でも撮影可能となります(PyAutoGUIでもスクリーンショット可能だが、中身はImageGrabらしいので速度的にこちらを使用) -
np.array(img)
:
画像比較時に画像をnumpy配列とする必要があるので変換 -
if 'img_mae' in locals()
:
ループ初回時はimg_mae
が未定義なので画像比較をしないための条件分岐。初回は比較せずに保存して、img_mae = img_ima
で次の比較用として代入 -
cv2.createBackgroundSubtractorMOG2()
:
背景差分用のインスタンス生成。OpenCVの背景差分を取得する関数。背景差分を取得する関数は色々あるので、他の関数でも問題はないはず
https://pystyle.info/opencv-background-substraction/ -
fgmask
:
生成したインスタンスに比較する画像を2つ適用すると差分がある領域が白(127)、差分がない領域が黒(0)の2値化データになります。 -
if np.count_nonzero(fgmask) / fgmask.size * 100 < 0.5
:
画面変化検知。背景差分の0(黒)ではない領域(=白)が全体の0.5%以下であれば、画面変化がないとして画像を保存せずにループを戻る。なぜ0.5%かというと、1ピクセルでも検知すると、マウスやテキストボックスの選択時のバー点滅など微妙な変化でもスクリーンショットを取り続けてしまうので、実用的にほぼ変化がないとできるのが0.5%だったため。 -
time.sleep
:
待機処理を一番最初に入れているのはtime.sleep
を1箇所のみにしたかったから(背景差分で保存しないときはcontinue
でループが戻るため)。PC環境によって異なるが、スクリーンショット→背景差分の処理で0.2~0.5秒ほどかかるので、もし指定秒数毎で厳密に待機させたい場合は処理中の時間を計ってstop_time
から差し引く必要があります
マウスカーソル位置の取得関数
try:
windll.shcore.SetProcessDpiAwareness(True)
except Exception:
pass
def position_get():
point = wintypes.POINT()
windll.user32.GetCursorPos(ctypes.byref(point))
return point.x, point.y
-
ctypes
でWindowsのAPIからマウスカーソル位置を取得する部分 - ネット上の情報では
POINT()
を独自クラスとして自分で定義する形がよく見ましたが、このctypes
で実装する前に使っていたpynput
の内部では上記のようにctypes.wintypes.POINT()
でインスタンス生成していたので、このコードとしています。 - 事前に読み込んでいる
windll.shcore.SetProcessDpiAwareness(True)
は高DPIディスプレイでスケール変更があった際の対応として入れておく必要があるらしいですが、現状私の環境では挙動を確認しきっていません。 -
wintypes
とwindll
はimport ctypes
単独ではインポートできず、from ctypes import windll, wintypes
と別途インポートする必要があります。
def screenshot_position():
input('取得したい箇所の"左上"にカーソルを当てEnterキー押してください')
x1, y1 = position_get()
print(f'X:{str(x1)}, Y:{str(y1)}')
input('取得したい箇所の"右下"にカーソルを当てEnterキー押してください')
x2, y2 = position_get()
print(f'X:{str(x2)}, Y:{str(y2)}')
with open('screenshot_position.txt', 'w') as f:
f.write(f'{str(x1)}, {str(y1)}, {str(x2)}, {str(y2)}')
-
スクリーンショット範囲を座標で設定するのは、ツールがないと実質不可能なので実装した関数
-
1度取得した座標は
screenshot_position.txt
に保存して、2回目以降に同じ範囲をスクリーンショットする場合には設定しなくてよい状態にしている
def main():
if not os.path.exists('screenshot_position.txt'):
print('スクリーンショット位置を設定してください')
screenshot_position()
else:
screenshot_set = input("スクリーンショットする位置を設定しますか?[y/any]")
if screenshot_set == "y" or screenshot_set == "Y":
screenshot_position()
x1, y1, x2, y2 = np.loadtxt('screenshot_position.txt', delimiter=',')
stop_time = input('撮影間隔(秒)を入力して、ENTERで開始(デフォルト値:2秒、最低値:1秒):')
stop_time = 2 if not stop_time else 1 if float(stop_time) < 1 else float(stop_time)
dir_path = dt.now().strftime('%y%m%d_%H%M')
if not os.path.exists(dir_path):
os.mkdir(dir_path)
print('\nスクリーンショットを開始しました\nCtl+cで終了できます')
screenshot_loop(stop_time, dir_path, x1, y1, x2, y2)
-
screenshot_position.txt
が存在しなければscreenshot_position()
を呼び出して設定→すでに設定済みなら、x1, y1, x2, y2
にtxtから読み込む -
stop_time
:
指定秒数設定は保存する画像の名前がスクリーンショットした時間で管理していることから、最低値を1秒として、条件に合わない場合などは補正するようにしている -
dir_path
:
保存するディレクトリは時間を名前に自動生成