[Python] PyAutoGUI 画像認識によるクリック・待機処理


# はじめに
PyAutoGUIで画像認識でクリックや待機処理をする方法を以下のページで記載した方法で実行していましたが、設定違いや複数画像対応などで関数自体が増えてしまいました。

そのため複数画像でも設定を変更する場合でも、臨機応変に対応可能な関数にまとめてみました。

https://qiita.com/takafi/items/48f146e1315ff19444bb

https://qiita.com/takafi/items/91a8480ef42cd240de28

2024/1/24追記：以下の関数についても本ページにまとめました。

https://qiita.com/takafi/items/be0433b251b2cfaed1cd


## 動作テスト環境
OS: Windows 10 Pro 64bit
言語： Python 3.9.13
ライブラリ：PyAutoGUI 0.9.53

# 1. クリックする関数

```Python:クリックする関数
import time
import pyautogui

def click_img(img: str, **kwargs):
    # 可変長引数を代入
    sleep_time = kwargs.pop('sleep_time', 0.01)  # クリック後の待機時間
    offset_x = kwargs.pop('offset_x', 0)         # 画像認識した座標とクリック座標をx方向にオフセット
    offset_y = kwargs.pop('offset_y', 0)         # 画像認識した座標とクリック座標をy方向にオフセット
    click_lr = kwargs.pop('click_lr', 'left')    # 左クリックor右クリック
    gray_scale = kwargs.pop('gray_scale', True)  # 画像認識時のグレースケールONOFF
    confidence = kwargs.pop('confidence', 0.8)   # 画像認識時の判定度合い
    # 画像の座標を代入
    x, y = pyautogui.locateCenterOnScreen(f'./img/{img}', grayscale=gray_scale, confidence=confidence)
    # 座標をクリック
    pyautogui.click(x + offset_x, y + offset_y, button=click_lr)
    # 指定時間の待機
    time.sleep(sleep_time)
```

```Python:呼び出し方
# 画像認識で取得した座標を左クリックして0.01秒待機
click_img('test.png')

# 画像認識で取得した座標のx:+40px, y:-50pxの位置を右クリックして、1秒待機
click_img('test.png', offset_x=40, offset_y=-50, sleep_time=1, click_lr='right')
```

* 画像のみ指定すれば、画像認識で取得した画像の中央をクリックする。
* `offset_x`, `offset_y`を指定すると座標をずらしてクリックが可能
* `sleep_time`：画像をクリック後の待機時間を設定可能
（デフォルト値の0.01秒はpyautoguiを使ってきた結果としての安定動作させるためのおまじない的な位置づけです）
* `click_lr`：右クリックをする場合に`click_lr='right'`で指定可能

# 2. 待機処理関数
## 2.1. 画像が表示されるまで待機

```Python:待機処理関数
import time
import pyautogui

def wait_img(*args, **kwargs):
    # 可変長引数を代入
    wait_limit = kwargs.pop('wait_limit', 60) * 5  # 待機時間（int）
    gray_scale = kwargs.pop('gray_scale', True)    # 画像認識時のグレースケールONOFF
    confidence = kwargs.pop('confidence', 0.8)     # 画像認識時の判定度合い
    # 待機時間ループ
    for _ in range(wait_limit):
        # 複数画像設定時のループ
        for img in args:
            try:
                # 画像認識に成功すればget_imgを定義
                if pyautogui.locateOnScreen(f'./img/{img}', grayscale=gray_scale, confidence=confidence):
                    get_img = img
                    break
            except pyautogui.ImageNotFoundException:
                pass
        # get_imgが定義されていればreturnでループ脱出
        if 'get_img' in locals():
            return get_img
        # returnで脱出するまで0.2秒待機を続ける
        time.sleep(0.2)
```

```Python:呼び出し方
# 画像が表示されるまで待機(待機時間60秒(デフォルト値))
wait_img('test.png')

# 画像が表示されるまで待機(待機時間1秒)
wait_img('test.png', wait_limit=1)

# 複数画像のどれかが表示されるまで待機
wait_img('test1.png', 'test2.png', 'test3.png')

# 複数画像のどれかが表示されるまで待機して、表示された画像をクリック
img = wait_img('test1.png', 'test2.png', 'test3.png')
click_img(img)

# リストにまとめている場合は、展開して渡せる
img_list = ['test1.png', 'test2.png', 'test3.png']
wait_img(*img_list)

# 以下のような表記で複数画像を待機してクリックも可能
click_img(wait_img(*img_list))
```

* 画像ファイル名は列挙すれば`*args`で可変長引数としてタプル化されて待機処理ループに組み込まれる。
* `wait_limit`は待機時間を変更したい場合に秒単位で指定可能
指定しても実際には画像認識などの処理に伴う遅延があるので指定時間よりは長くなります
またforループで回しているためint型にする必要があります（floatだとエラーが出る）
* 戻り値として画像ファイル名を返すようにしているので、複数画像を指定した際には変数へ代入して、先に示した`click_img()`へ入れれば、待機処理後にクリックできます。
* リストやタプルを展開して関数に渡す挙動については（https://note.nkmk.me/python-argument-expand/ ）を参照してください。

## 2.2. 画像が表示されなくなるまで待機

```Python:待機処理関数
import time
import pyautogui

def wait_while_img_is_displayed(*images, **options):
    # 可変長引数を代入
    wait_limit = options.pop('wait_limit', 60) * 5  # 待機時間（int）
    gray_scale = options.pop('grayscale', True)     # 画像認識時のグレースケールONOFF
    confidence = options.pop('confidence', 0.8)     # 画像認識時の判定度合い
    get_img = [True for _ in range(len(images))]
    # 待機時間ループ
    for _ in range(wait_limit):
        # 複数画像設定時のループ
        for i, img in enumerate(images):
            try:
                # 画像認識しなければget_imgをFalseにする
                if pyautogui.locateOnScreen(f'./img/{img}', grayscale=gray_scale, confidence=confidence) is None:
                    get_img[i] = False
            except pyautogui.ImageNotFoundException:
                get_img[i] = False
        # get_imgがすべてFalseになったらループを抜ける
        if not any(get_img):
            return True
        # 0.2秒待機
        time.sleep(0.2)
    return False
```

* 基本的な構成は画像が表示されるまで待機する場合と同じですが、`pyautogui.locateOnScreen`の判定を`is None`とした点、画像が表示されなくなったことを配列がすべて`False`になったことで判定する点を変更しています。


# 運用例
## 1) Googleで検索
Chromeを起動して、Googleにアクセスして、検索窓に検索ワードを入力してENTERする処理の流れ。
```Python:
import time
import pyautogui
import pyperclip
import subprocess

# Chromeを起動
subprocess.Popen(r"C:\Program Files\Google\Chrome\Application\chrome.exe")
# Chromeが起動するまで待機(chrome.pngはChromeの起動時に表示されるどこかの画像)
wait_img('chrome.png')
# 新しいタブを表示
pyautogui.hotkey('ctrl', 't')
# URLをクリップボードにコピー
pyperclip.copy('https://www.google.co.jp/')
# URLを貼り付けてENTER
pyautogui.hotkey('ctrl', 'v')
pyautogui.press('enter')
# 表示されるまで待機
wait_img('google.png')
# 検索窓をクリック
click_img('google.png', offset_y=100)
# 検索ワードを入力してENTER
pyautogui.typewrite('検索ワード', interval=0.001)
pyautogui.press('enter')
```

* プログラムの起動に`subprocess`、URLなど一部の`pyautogui.typewrite`に未対応の文字(:, @, ^)用に`pyperclip`を組み合わせれば、大抵の処理は人間が意識して実行する流れで記述できるという印象です。
参考：https://teratail.com/questions/79973

## 2) Teamsに２段階認証を使ってログイン
```Python:
import time
import pyautogui
import pyperclip
import subprocess

# ユーザ名・パスワードを入力
username = input("Teamsのユーザ名を入力してください：")
password = input("Teamsのパスワードを入力してください：")
# Teamsを起動
subprocess.Popen(f'C:\\Users\\user_name\\AppData\\Local\\Microsoft\\Teams\\current\\Teams.exe --processStart "Teams.exe"')
# Teamsのログイン画面が起動するまで待機してクリック(teams01.pngはTeamsのユーザ名入力窓の画像を想定)
click_img(wait_img('teams01.png'))
# ユーザ名を入力してENTER
pyautogui.typewrite(username, interval=0.001)
pyautogui.press('enter')
# パスワード入力欄が表示されるまで待機してクリック
click_img(wait_img('teams02.png'))
# ユーザ名を入力してENTER
pyautogui.typewrite(password, interval=0.001)
pyautogui.press('enter')
# ２段階認証コード入力欄が表示されるまで待機
wait_img('teams03.png')
# ２段階認証コードの入力が終わって入力欄が表示されなくなるまで待機
wait_while_img_is_displayed('teams03.png')
```