算出したゆいもっぷちゃんの特徴点をSmoothGraphicに自動で入力させるためにpyautoguiをマスターした

# 経緯

- 前回(2023/11)、SmoothGraphic へ入力する特徴点を自動で(かなり自動化率の高い工程で)出せるプログラムを作った
    - https://qiita.com/ystmg/items/48e5a86fd3a44bfd35c6
- 今回、その算出した特徴点を自動で入力できるように Pythonの プログラムを書いた
- 今回の話を始めたのは 2024/2/23
    - VALORANTのためにWindows7機を復活させてWindows10にしたのが終わったのが 2/22 なので、その翌日からである
        - https://t-hazawa.hatenablog.com/entry/2024/02/22/Windows7_%E3%82%92_Windows10_%E3%81%AB%E3%82%A2%E3%83%83%E3%83%97%E3%82%B0%E3%83%AC%E3%83%BC%E3%83%89%E3%81%97%E3%81%9F
    - 多分、この頃に、「12/3以降3ヶ月弱アニメ作りのことをやってないからそろそろ再開したい」という気分が高まったみたい

# 既製品をためした

- 意外と、スペース+クリック(SmoothGraphicで2枚めの特徴点を指定した位置に移動させる操作)を実現できるフリーソフトはなかった

# SmoothGraphicのポイント (復習)

- 画像を読み込ませようとして、手動設定しようとして表示が透明だったら、ペイント2Dで上書き保存するとうまくいく
- スペース+クリックするときには、他の入力補助ソフト(キーバインド変えるものなど)を止めないといけない
    - pyautogui を使う時も入力補助ソフトを止めないといけない

# 技術選定

- Windows なので PowerShell も候補
    - https://qiita.com/falcatamaster/items/ea213640f347b2ac5cda 
    - でも、Windowsプリインストールみたいな縛りもないし、 自分にはpyautoguiの方が利用可能範囲が広そうなのでそっちに
        - (触った技術の幅としては触ったことがない Windows PowerShell を触る方が広がったけど)

## pyautogui

- https://one-div.com/programming/python/what-is-pyautogui
- ☆特にお世話になった記事↓ (まずこれを模倣してみた)
    - https://qiita.com/run1000dori/items/301bb63c8a69c3fcb1bd
    - ☆任意の画像を指定してpositionを取れるのは大変いいね (ボタンの画像をローカルに保存しておいて指定すると、その画像と一致する部分をクリックできる)
    - dragToもあるね 2024/02/28 22:49
    - 時間指定もあるよ　2024/02/28 22:50
    - > 文字を入力するのではなく、単にキーを押したい場合はpyautogui.press()を使います。
- やった結果、やはり Python は楽ちんだった

# 設計

## 元の特徴点算出プログラムで出力する特徴点座標テキストファイル

<pre>1,2 #left eye leftmost
5,6 #left eye rightmost
</pre>

- ↑が画像ごとにあって、コメント部分が同じものを連続して(SmoothGraphic上で)クリックしていくことにした

# 技術検証

- スペース押しながらクリックできるかまずやった
- 参考にさせて頂いた記事
    - https://self-development.info/python%E3%81%A7%E3%82%AD%E3%83%BC%E3%83%9C%E3%83%BC%E3%83%89%E6%93%8D%E4%BD%9C%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%99%E3%82%8B%E3%80%90pyautogui%E3%80%91/
- keyDown(), keyUp() らしい
    - → (対象のWindowに)フォーカスがのってれば大事ょうぶなのがわかった！2024/03/02 22:23
    - と思ったがフォーカスなくても大丈夫だった(ようだ)


# 開発

- 今回も ChatGPT Plus の GPT-4 (プログラミング、特にPythonのプログラミングがとても得意)に書きまくってもらった

- (最初は1つの入力テキストファイルにニ枚の画像の特徴点情報を入れる設計だった)(設計ミス)
  
<pre>Pythonで、以下のテキストファイル 
C:\Users\ao_live_yuimop\py_sousa_rens\kenkyu_click_points.txt を読み込んで、
pyautogui でクリックしやすいようにいい感じに座標点群を読み込んでほしいです。

[入力テキストファイル]
109,197 #imageA left eye leftmost
127,293 #imageB left eye leftmost
144,192 #imageA left eye rightmost
164,283 #imageB left eye rightmost
ー
Pythonでテキストファイルから座標点を読み込んで、`pyautogui`を使用して
クリック操作を行うためのコード例を示します。
この例では、ファイルから座標を読み込み、それぞれの座標に対して
クリック操作を行います。
ただし、実際にクリック操作を行う前に、`pyautogui`の動作速度や
クリック間の遅延などを適切に設定することが重要です。

```python
import pyautogui
import time

# ファイルパス（Windowsのパス形式に注意）
file_path = r"C:\Users\ao_love_yuimop\py_sousa_rens\kenkyu_click_points.txt"

# ファイルから座標を読み込む関数
def load_click_points(file_path):
    points = []
    with open(file_path, 'r') as file:
        for line in file:
            # コメントを無視して、座標を取得
            point_str = line.split('#')[0].strip()
            if point_str:
                # 文字列から座標のタプルに変換
                points.append(tuple(map(int, point_str.split(','))))
    return points

# 座標に対してクリック操作を行う関数
def click_points(points, delay=0.5):
    for point in points:
        # 座標に移動
        pyautogui.moveTo(point)
        # クリック
        pyautogui.click()
        # 次のクリックまでの遅延
        time.sleep(delay)

# 座標点をファイルから読み込む
points = load_click_points(file_path)

# 読み込んだ座標点に対してクリック操作を行う
click_points(points)
```

このスクリプトでは、まず`load_click_points`関数で
ファイルから座標点群を読み込みます。
各行から`#`で始まるコメントを無視し、カンマで
区切られた座標値をタプルとしてリストに追加しています。

次に、`click_points`関数を使用して、読み込んだ座標点に対して
`pyautogui.moveTo`と`pyautogui.click`を使ってクリック操作を行います。
`delay`パラメータは、連続するクリック操作の間に挿入する遅延時間を指定します。

このコードを実行する前に、`pyautogui`ライブラリが
インストールされていることを確認してください。
もしインストールされていない場合は、
コマンドラインで`pip install pyautogui`を実行してインストールできます。
また、実際にクリック操作を行う前に、スクリプトが予期しない動作を
しないように、`pyautogui`のドキュメントを確認し、
必要に応じて安全機能（例えば、移動速度の設定）を適切に設定してください。
</pre>

- ↑色々と間違ってるけど、対応するコメントを連続してクリックして、みたいに指示していったら正しくなっていった


# 運用

- SmoothGraphic をクリックするには、画像Aと画像Bのウィンドウ位置を取る必要があったので、カーソル位置の座標をとるソフトを探した
- MPP Utility 1.20 が良かった
- ここまでが3/5で、確定申告をしていたので再開は 3/18 (L490)

# さらに開発

## 今回やったアウトライン

<pre>
A:
1. テスト画像組で座標データ作る
2. お手製ソフトで、SmoothGraphicをクリックできるか試す

B:
1. 元の特徴点出すシステムをクリックシステムの形式で出力させる
2. とく頂点シすテムで、SmmothGraphic クリックできるか試す
3. それで試しに動画を作ってみる(1間隙)(やってない)
</pre>

- プインと
    - 特徴点抽出プログラムでは、目的の点が見つからなかったというか画面内になかった時は、(0,0) ... ではなく (1,1) を返してるので、その時はクリックしない(スキップする)
        - Pythonは座標を表す型？(Point型？)を (X,Y)と表記してるっぽい 


## 元の特徴点出すプログラムは画像ごとの処理だったので、クリックさせるプログラムの型の座標入力ファイルを2つに改修する

- 元の頂点出すシステムで今回の形式で座標だすぞ だした2024/03/25 21:18
- やはりChatGPT頼り

<pre>(↓の指示では正しく伝わらなかった)

現在、下記のコードでクリックをさせています。
現在の座標の入力ファイルは1つで、その入力ファイルに、
クリックする順番で座標が並んでいます。

入力ファイルを画像ごとに分けて、2つにしたいです。
座標の後ろについているコメントが一致するものについて、
画像A→画像Bの順番でクリックするように改修してください。

[現在の座標入力ファイル]
163,196 #imageA left chara miken
175,283 #imageB left chara miken
337,138 #imageA right chara miken
319,261 #imageB right chara miken

[改修後の座標入力ファイル(画像A)]
163,196 #left chara miken
337,138 #right chara miken

[改修後の座標入力ファイル(画像B)]
175,283 #left chara miken
319,261 #right chara miken

[現在のコード]
# !! 実行前に入力補助ソフトをオフにする必要がある
# PyAutoGUIライブラリのインポート
import pyautogui

# timeモジュールのsleep関数をインポート
from time import sleep

print("start")

import pyautogui
import time

# ファイルパス（Windowsのパス形式に注意）
FILE_PATH = r"C:\Users\ao_love_yuimop\py_sousa_rens\click_points_test.txt"
IMAGE_A_X_OFFSET = 0
IMAGE_A_Y_OFFSET = 52
IMAGE_A_X_END = 852
IMAGE_A_Y_END = 1334
IMAGE_B_X_OFFSET = 866
IMAGE_B_Y_OFFSET = 57

ORIGINAL_IMAGE_X = 512
ORIGINAL_IMAGE_Y = 768

# ファイルから座標を読み込み、画像AとBの座標に分けて返す関数
def load_click_points(FILE_PATH):
    points_a = []
    points_b = []
    with open(FILE_PATH, 'r') as file:
        lines = file.readlines()
        for i, line in enumerate(lines):
            # コメントを無視して、座標を取得
            point_str = line.split('#')[0].strip()
            if point_str:
                point = tuple(map(int, point_str.split(',')))
                # 奇数行（0-indexedで偶数）は画像A、偶数行（0-indexedで奇数）は画像B
                if i % 2 == 0:
                    points_a.append(((point[0] * (IMAGE_A_X_END - IMAGE_A_X_OFFSET) / ORIGINAL_IMAGE_X) + IMAGE_A_X_OFFSET, (point[1] * (IMAGE_A_Y_END - IMAGE_A_Y_OFFSET) / ORIGINAL_IMAGE_Y) + IMAGE_A_Y_OFFSET))
                else:
                    # 画像Bの場合はX座標に+500
                    points_b.append(((point[0] * (IMAGE_A_X_END - IMAGE_A_X_OFFSET) / ORIGINAL_IMAGE_X) + IMAGE_B_X_OFFSET, (point[1] * (IMAGE_A_Y_END - IMAGE_A_Y_OFFSET) / ORIGINAL_IMAGE_Y) + IMAGE_B_Y_OFFSET))
    return points_a, points_b

# 座標に対してクリック操作を行う関数
def click_points(points_a, points_b, delay=0.5): #0.5):
    for point_a, point_b in zip(points_a, points_b):
        # 画像Aの座標をクリック
        pyautogui.moveTo(point_a)
        pyautogui.click()
        time.sleep(delay)

        # 画像Bの座標をスペースキーを押しながらクリック
        pyautogui.moveTo(point_b)
        pyautogui.keyDown('space')
        pyautogui.click()
        pyautogui.keyUp('space')
        time.sleep(delay)

# 座標点をファイルから読み込む
points_a, points_b = load_click_points(FILE_PATH)

# 読み込んだ座標点に対してクリック操作を行う
click_points(points_a, points_b)

ー
改修を行うには、現在のコードと座標入力ファイルの
処理ロジックを変更する必要があります。
目的は、コメントに基づいて画像Aと画像Bの座標を分けて、
それぞれを別々に処理することです。

まず、座標入力ファイルをコメントに基づいて画像ごとに分けるために、
`load_click_points`関数を次のように修正します。
コメント内の"imageA"または"imageB"のラベルに基づいて座標を分類し、
対応するリストに追加するロジックを実装します。

```python
# ファイルから座標を読み込み、コメントに基づいて画像AとBの座標に分けて返す関数
def load_click_points(file_path):
    points_a = []
    points_b = []
    with open(file_path, 'r') as file:
        lines = file.readlines()
        for line in lines:
            parts = line.strip().split('#')
            if len(parts) == 2:
                point_str, comment = parts
                point = tuple(map(int, point_str.split(',')))
                # コメントに基づいて画像Aまたは画像Bのリストに座標を追加
                if 'imageA' in comment:
                    points_a.append(point)
                elif 'imageB' in comment:
                    points_b.append(point)
    return points_a, points_b
```

この関数は、入力ファイルから座標とコメントを読み込み、
コメントの内容に基づいてそれぞれの座標を画像Aまたは画像Bに分類します。
修正された関数は、画像Aと画像Bの座標を別々のリストとして返します。

次に、`click_points`関数は既に画像Aと画像Bの座標を
別々に処理するように設計されているため、
この関数自体の修正は必要ありません。
ただし、関数を呼び出す前に、新しい`load_click_points`関数を
使用して座標を正しく読み込むことが重要です。

最後に、座標入力ファイルをコメントに基づいて2つに分けたい
とのことなので、提供されたフォーマットに従って
2つの新しいファイルを作成し、適切な座標をそれぞれの
ファイルに記述する必要があります。しかし、このプロセスは手動で行うか、
別途スクリプトを書いて自動化することができますが、
上記のコード変更は既存のシングルファイルから座標を適切に分類し、
プログラム内で処理するためのものです。

修正後、プログラムは入力ファイルから画像ごとの座標を読み込み、
画像Aのすべての座標をクリックした後、
画像Bのすべての座標をクリックするように動作します。


ーー
(↑で一発でいかなかったので追加注文したらうまくいった)
そうではなく、入力ファイルが下記でも動くように改修してください。  

[改修後の座標入力ファイル(画像A)]
163,196 #left chara miken
337,138 #right chara miken

[改修後の座標入力ファイル(画像B)]
175,283 #right chara miken
319,261 #left chara miken

ー
入力ファイルを2つに分けて、それぞれのファイルから座標を読み込んで、
画像Aと画像Bの順番でクリックするようにコードを
改修する必要があります。以下に、改修後のコードを示します。

まず、画像Aと画像Bの座標を読み込むための関数をそれぞれ用意し、
コメントをキーとして座標を辞書に格納します。
これにより、コメントが一致する座標同士を
正確にマッチングさせることができます。

```python
import pyautogui
import time

# 画像Aと画像Bのファイルパス
FILE_PATH_A = r"path\to\imageA_coordinates.txt"
FILE_PATH_B = r"path\to\imageB_coordinates.txt"

# 座標を読み込み、コメントをキーとする辞書を返す関数
def load_click_points(file_path):
    points = {}
    with open(file_path, 'r') as file:
        for line in file:
            parts = line.strip().split('#')
            if len(parts) == 2:
                point_str, comment = parts
                points[comment.strip()] = tuple(map(int, point_str.split(',')))
    return points

# 座標点をファイルから読み込む
points_a = load_click_points(FILE_PATH_A)
points_b = load_click_points(FILE_PATH_B)

# 座標に対してクリック操作を行う関数
def click_points(points_a, points_b, delay=0.5):
    # コメントが一致する座標同士でクリック操作を行う
    for comment in points_a.keys():
        if comment in points_b:
            point_a = points_a[comment]
            point_b = points_b[comment]
            
            # 画像Aの座標をクリック
            pyautogui.moveTo(point_a)
            pyautogui.click()
            time.sleep(delay)

            # 画像Bの座標をスペースキーを押しながらクリック
            pyautogui.moveTo(point_b)
            pyautogui.keyDown('space')
            pyautogui.click()
            pyautogui.keyUp('space')
            time.sleep(delay)

# 読み込んだ座標点に対してクリック操作を行う
click_points(points_a, points_b)
```

このコードでは、`load_click_points`関数がファイルパスを引数に取り、
そのファイルから座標とコメントを読み込んで、
コメントをキーとする辞書に格納します。
この辞書を使用して、`click_points`関数では
画像Aと画像Bでコメントが一致する座標に対して
順番にクリック操作を行います。

この方法により、入力ファイルを画像ごとに分けて、
かつコメントに基づいて正確にマッチングさせることが可能になります。
ファイルパスは実際の環境に合わせて適切に設定してください。
</pre>

## 特徴点出すプログラムの方で座標をファイル出力させる

<pre>
Pythonです。
regionsは print(regions) すると
{'bangs': {'start': 0, 'end': 239, 'center': (611, 119)}, 'face': {'start': 288, 'end': 385, 'center': (611, 336)}, 'mouth': {'start': 320, 'end': 358, 'center': (611, 339)}, 'neck_shadow': {'start': 390, 'end': 461, 'center': (611, 425)}, 'upper_body_clothing': {'start': 512, 
'end': 548, 'center': (611, 530)}, 'lower_body_clothing': {'start': 1, 'end': 1, 'center': (1, 1)}, 'eye_0': {'start': 229, 'end': 285, 'center': (559, 257)}, 'eye_1': {'start': 232, 'end': 283, 'center': (663, 257)}}

以下の内容・形式でテキストファイルに出力するコードを書いてください。

[内容・形式]
611,0 #bangs start
611,119 #bangs center
611,239 #bangs end
611,288 #face start
611,336 #face center
611,385 #face end
...
663,232 #eye_1 start
663,257 #eye_1 center
663,283 #eye_1 end

ー

以下のPythonコードは、指定された`regions`辞書から、
要求された内容と形式でテキストファイルに出力します。
このコードでは、各リージョンについて、
`start`、`center`、`end`の順に座標をファイルに書き込みます。

```python
regions = {
    'bangs': {'start': 0, 'end': 239, 'center': (611, 119)},
    'face': {'start': 288, 'end': 385, 'center': (611, 336)},
    'mouth': {'start': 320, 'end': 358, 'center': (611, 339)},
    'neck_shadow': {'start': 390, 'end': 461, 'center': (611, 425)},
    'upper_body_clothing': {'start': 512, 'end': 548, 'center': (611, 530)},
    'lower_body_clothing': {'start': 1, 'end': 1, 'center': (1, 1)},
    'eye_0': {'start': 229, 'end': 285, 'center': (559, 257)},
    'eye_1': {'start': 232, 'end': 283, 'center': (663, 257)}
}

# 出力ファイルを開く
with open('output.txt', 'w') as file:
    # regions辞書をイテレート
    for region, details in regions.items():
        # start, center, end の順に座標を書き込む
        x_center, y_center = details['center']
        file.write(f"{x_center},{details['start']} #{region} start\n")
        file.write(f"{x_center},{y_center} #{region} center\n")
        file.write(f"{x_center},{details['end']} #{region} end\n")

print("Done writing to output.txt.")
```

このコードを実行すると、`output.txt` ファイルに要求された情報が指定された形式で書き込まれます。`with open('output.txt', 'w') as file:` はファイルを開いて書き込みモードで操作し、処理が完了したら自動的にファイルを閉じます。`regions` 辞書をループして、各リージョンの`start`、`center`、`end`の座標を書き込んでいます。この際、`center`はタプルからX座標とY座標を分解して使用します。
</pre>

## その他ポイント

- SmoothGraphicのウィンドウサイズは元の画像サイズと縮尺が違う。
- 特徴点はオリジナルサイズで出すので、ウィンドウサイズに合わせて座標を調整せなかん

# 最終的にできた2つのテキストファイルの座標をクリックさせるPythonプログラム

<pre># !! 実行前に入力補助ソフトをオフにする必要がある
# PyAutoGUIライブラリのインポート
import pyautogui

# timeモジュールのsleep関数をインポート
from time import sleep
import time

print("start")
sleep(2)

# ファイルパス（Windowsのパス形式に注意）
FILE_PATH_A = r"C:\Users\ao_love_yuimop\py_sousa_rens\points\gen1_points.txt"
FILE_PATH_B = r"C:\Users\ao_love_yuimop\py_sousa_rens\points\gen2_points.txt"

IMAGE_A_X_OFFSET = 0
IMAGE_A_Y_OFFSET = 56 #50
IMAGE_A_X_END = 1285 #852
IMAGE_A_Y_END = 905 #1334
IMAGE_B_X_OFFSET =  IMAGE_A_X_END + 14 #1299 #866
IMAGE_B_Y_OFFSET =  IMAGE_A_Y_OFFSET #57

ORIGINAL_IMAGE_X = 1200 #512
ORIGINAL_IMAGE_Y = 800 #768

# 座標を読み込み、コメントをキーとする辞書を返す関数
def load_click_points(file_path):
    points = {}
    with open(file_path, 'r') as file:
        for line in file:
            parts = line.strip().split('#')
            if len(parts) == 2:
                point_str, comment = parts
                points[comment.strip()] = tuple(map(int, point_str.split(',')))
    return points

# 座標に対してクリック操作を行う関数
def click_points(points_a, points_b, delay=0.5):    
    # コメントが一致する座標同士でクリック操作を行う
    for comment in points_a.keys():
        if comment in points_b:
            # points_a もしく points_b のどちらかが(1, 1)だったらスキップ
            if points_a[comment] == (1, 1) or points_b[comment] == (1, 1):
                continue

            point_a = points_a[comment]
            point_b = points_b[comment]

            # 両方の座標にオフセットをつけたり拡大縮小したりする
            point_a = (point_a[0] * (IMAGE_A_X_END - IMAGE_A_X_OFFSET) // ORIGINAL_IMAGE_X + IMAGE_A_X_OFFSET,
                       point_a[1] * (IMAGE_A_Y_END - IMAGE_A_Y_OFFSET) // ORIGINAL_IMAGE_Y + IMAGE_A_Y_OFFSET)
            point_b = (point_b[0] * (IMAGE_A_X_END - IMAGE_A_X_OFFSET) // ORIGINAL_IMAGE_X + IMAGE_B_X_OFFSET,
                       point_b[1] * (IMAGE_A_Y_END - IMAGE_A_Y_OFFSET) // ORIGINAL_IMAGE_Y + IMAGE_B_Y_OFFSET)
            
            # 画像Aの座標をクリック
            pyautogui.moveTo(point_a)
            pyautogui.click()
            time.sleep(delay)

            # 画像Bの座標をスペースキーを押しながらクリック
            # まず、画像BのWindowにフォーカスを当てるためにオフセットから上と右に15進んだ点をクリック …は必要なかった
            #pyautogui.moveTo(IMAGE_B_X_OFFSET + 12, IMAGE_B_Y_OFFSET - 12)
            #pyautogui.click()
            #time.sleep(delay)
            pyautogui.moveTo(point_b)
            pyautogui.keyDown('space')
            pyautogui.click()
            pyautogui.keyUp('space')
            time.sleep(delay)

# 座標点をファイルから読み込む
points_a = load_click_points(FILE_PATH_A)
points_b = load_click_points(FILE_PATH_B)

# 読み込んだ座標点に対してクリック操作を行う
click_points(points_a, points_b)
</pre>

# 感想

- Python でWindowsを操作できるようになって良かった

# 所要時間

- 今回の話を始めたのは 2024/2/23
- 3/5まで途中までやった (14日間 = 140分 = 2時間20分)
- 確定申告が終わって3/18 から再開
- プログラムが完成したのは 4/14
    - 27日間  = 270分 
    - なのでトータル 410分 = 7時間弱
    - 実際はその 2/3 の 4時間半くらいかな？
- 記事を書き始めたのは4/16
    - 記事を書き終えたのは2024/04/25 で 9日間 = 1時間半
    - 実際はその 2/3 の 1時間くらいかな？

# 次

- 秘策を調査&導入してゆいもっぷアニメを作る