【python】マウスドラッグで画像から範囲指定する

##やりたいこと
よくトリミングツール等で見かけるような"画像からの範囲指定”を、
ガイドラインを表示させながらマウスドラッグで可能にしたいです。↓ イメージはこんな感じ↓
![画像領域取得デモ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/70f9ca51-f7fa-f01d-522d-b1443bd346f0.gif)

##環境
Windows10 Home
Python3.8.2（32bit）
Visual Studio Code 1.45.1

##勉強したこと（流れ）
1. tkinterウィンドウに任意の画像を表示する
2. 表示した画像上に矩形（四角形）を描く
3. マウスドラッグによって矩形を変形する
4. 矩形の座標情報を得る


#### 1. tkinterウィンドウに任意の画像を表示する
今回のサンプルではスクリーンショットに対し矩形（ガイドライン）を描画していく予定です。
そのため、まずpyautoguiライブラリの`screenshot()`を利用してスクリーンショットを取得します。
用意した画像はそのままtkinter内で使用できないため、**ImageTk.PthoImageを使用しtkinter内で表示できる形式に変換**します。
`img_tk = ImageTk.PhotoImage(img)`
（この変換はTk()の後ろでないと「変換するのが早すぎ！」っぽいエラーが出るので注意）
（ImageTkはPillowのメソッドのため、別途Importする必要あり）

その後、tkinterでCanvasウィジェットを作成し、その中に画像を配置します。

```python
canvas1 = tkinter.Canvas(root, bg="black", width=img.width, height=img.height)
canvas1.create_image(0, 0, image=img_tk, anchor=tkinter.NW)
```

Canvasは画像にあわせて自動変形しないため、ウィジェットサイズを画像と同じになるように指定する必要があります。

create_imageのオプション`anchor`は「配置する画像のどこを基準にするか」を指定します。
値は`tkinter.`と東西南北で表記され、N：North ⇒ 北=上というイメージです。
N以外は W：左、S：下、E：右、CENTER：中心と書きます。
例の場合、img_tkの画像のNW = 左上 をx座標=0、ｙ座標=0（第一引数・第二引数で指定）に配置することになります。

#### 2. 表示した画像上に矩形（四角形）を描く
まず、Canvasでドラッグ開始されたときの動作を指定します。

```python
canvasウィジェット.bind("<ButtonPress-1>", <コールバック関数>)
```
呼び出したコールバック関数内でガイドライン描画処理を行ないます。（rectangleは矩形の意味）

```python
Canvasウィジェット.create_rectangle(始点のx座標, 始点のy座標, 終点のx座標, 終点のy座標, outline="線の色" ,tag="タグ名")
```

のちに描画した矩形を変形したりするので、オプション`tag`でタグ名を指定する必要があります。

コールバック関数宣言時に引数eventを指定しておくと、`event.x, event.y` で
クリック時のマウス座標を得ることが出来るので、この値を始点座標にセットします（終点は適当）

#### 3. マウスドラッグによって矩形を変形する
次に、Canvasでドラッグ中マウスが動いたときの動作を指定。

```python
Canvasウィジェット.bind("<Button1-Motion>", <コールバック関数>)
```

呼び出したコールバック関数内でガイドラインを変形する処理を記述します。

```python
Canvasウィジェット.coords("変形させる図形のタグ名", 始点のx座標, 始点のy座標, 終点のx座標, 終点のy座標)
```
で描画済みの図形のサイズを変更することが出来ます。（coordsはコーディネートの意味らしい）
ドラッグで矩形サイズを変更する際、以下２点のイレギュラーに注意しました。

######①始点よりも左or上にドラッグされた場合の対処
何も考えずに始点の座標に現在の図形座標をセットすると、
ドラッグ中のマウスが始点より左or上に移動した際にバグります。。
（始点の座標情報がドラッグ後の座標に書き換えられてしまう）
![画像領域取得失敗2デモ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642673/47d2bfdd-23e1-145a-2672-0ad37792c90f.gif)
そのため、前述のマウスクリック時に座標情報をgrobal変数に格納し、その値を再描画時の始点座標とすることで、
入れ替わりを防ぎました↓

```python
# ドラッグ開始した時のイベント - - - - - - - - - - - - - - - - - - - - - - - - - - 
def start_point_get(event):
    global start_x, start_y # グローバル変数に書き込みを行なうため宣言
　　　　：
　　（図形描画）　　　
　　　　:
    # グローバル変数に座標を格納
    start_x, start_y = event.x, event.y

# ドラッグ中のイベント - - - - - - - - - - - - - - - - - - - - - - - - - - 
def rect_drawing(event):
   　　 ：
    # "rect1"タグの画像を再描画
    canvas1.coords("rect1", start_x, start_y, event.x, event.y)
```


######②マウスが画面領域外にはみ出してしまった時の対処
今回のケースでは、マウスが描画領域の外にでてしまった場合、画面端の座標を保持するようにします。
そのため、マウス座標が「描画領域（0＜x≦画像の幅）」「描画領域外（x＜0）」「描画領域外（幅＜x）」かによって、
終点座標を書き換える必要があります。
if文を二つ重ねても良いですが、今回はmin関数を使うことでコードを短くしてみました。
（並び替えればmax関数でもOK）

```python
    if event.x < 0:
        end_x = 0 # 取得した座標が0以下の場合は0をセット
    else:
        end_x = min(img.width, event.x) # 画像の幅か取得した座標のうち小さい方をセット
```


#### 4. 矩形の座標情報を得る

ドラッグ終了したときに座標取得できるよう動作を指定します。

```python
Canvasウィジェット.bind("<ButtonRelease-1>", <コールバック関数>)
```

前述の`coords`メソッドを使用すると、図形の座標を取得することができます。

```python
start_x, start_y, end_x, end_y = Canvasウィジェット.coords("タグ名")
```

今回のケースでは全画面のスクリーンショットをtkウィンドウに表示するのが難しいので、
いくらか縮小したものを表示して、その上に矩形を描画しています。
そのため、リアルサイズの座標を得たい場合、`coords`で得た座標に、縮小倍率を掛ける必要があります。
今回はリスト内包表記を使って、コードができるだけ短くなるように書いてみました。

```python
    start_x, start_y, end_x, end_y = [
        round(n * RESIZE_RETIO) for n in canvas1.coords("rect1")]
```

以上が一連の流れです。お疲れさまでした。

## 完成したコード

```python
import tkinter
import time
import pyautogui  # 外部ライブラリ
from PIL import Image, ImageTk  # 外部ライブラリ

RESIZE_RETIO = 2 # 縮小倍率の規定

# ドラッグ開始した時のイベント - - - - - - - - - - - - - - - - - - - - - - - - - - 
def start_point_get(event):
    global start_x, start_y # グローバル変数に書き込みを行なうため宣言

    canvas1.delete("rect1")  # すでに"rect1"タグの図形があれば削除

    # canvas1上に四角形を描画（rectangleは矩形の意味）
    canvas1.create_rectangle(event.x,
                             event.y,
                             event.x + 1,
                             event.y + 1,
                             outline="red",
                             tag="rect1")
    # グローバル変数に座標を格納
    start_x, start_y = event.x, event.y

# ドラッグ中のイベント - - - - - - - - - - - - - - - - - - - - - - - - - - 
def rect_drawing(event):

    # ドラッグ中のマウスポインタが領域外に出た時の処理
    if event.x < 0:
        end_x = 0
    else:
        end_x = min(img_resized.width, event.x)
    if event.y < 0:
        end_y = 0
    else:
        end_y = min(img_resized.height, event.y)

    # "rect1"タグの画像を再描画
    canvas1.coords("rect1", start_x, start_y, end_x, end_y)

# ドラッグを離したときのイベント - - - - - - - - - - - - - - - - - - - - - - - - - - 
def release_action(event):

    # "rect1"タグの画像の座標を元の縮尺に戻して取得
    start_x, start_y, end_x, end_y = [
        round(n * RESIZE_RETIO) for n in canvas1.coords("rect1")
    ]

    # 取得した座標を表示
    pyautogui.alert("start_x : " + str(start_x) + "\n" + "start_y : " +
                    str(start_y) + "\n" + "end_x : " + str(end_x) + "\n" +
                    "end_y : " + str(end_y))

# メイン処理 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
if __name__ == "__main__":

    # 表示する画像の取得（スクリーンショット）
    img = pyautogui.screenshot()
    # スクリーンショットした画像は表示しきれないので画像リサイズ
    img_resized = img.resize(size=(int(img.width / RESIZE_RETIO),
                                   int(img.height / RESIZE_RETIO)),
                             resample=Image.BILINEAR)

    root = tkinter.Tk()
    root.attributes("-topmost", True) # tkinterウィンドウを常に最前面に表示

    # tkinterで表示できるように画像変換
    img_tk = ImageTk.PhotoImage(img_resized)

    # Canvasウィジェットの描画
    canvas1 = tkinter.Canvas(root,
                             bg="black",
                             width=img_resized.width,
                             height=img_resized.height)
    # Canvasウィジェットに取得した画像を描画
    canvas1.create_image(0, 0, image=img_tk, anchor=tkinter.NW)

    # Canvasウィジェットを配置し、各種イベントを設定
    canvas1.pack()
    canvas1.bind("<ButtonPress-1>", start_point_get)
    canvas1.bind("<Button1-Motion>", rect_drawing)
    canvas1.bind("<ButtonRelease-1>", release_action)

    root.mainloop()

```

##今後の予定
画像から範囲指定するインターフェースができたので、色んなツールにつなげていきたいです。
例えば「スクリーンショット画面から連続で切り抜き⇒現在時刻で名前をつけて保存」するツールや、
「指定範囲からOCR」するツールなど。
スクリーンショット画面だけではなく、クリップボードの画像を処理するツールなども面白いかもしれません。

##参考にさせていただいたサイト
[Pythonのcanvasに表示した四角形を変形する](https://water2litter.net/rum/post/python_tkinter_canvas_rectangle_modification/)