Pythonで好きな場面をクロップできるソフトを作ってみた::マウスで簡単にエリア選択できます

2021年6月19日

今日はマウスで好きな場面をクロップできるソフトを紹介します。

本プログラムの特徴

  • tkinterでGUIを作成
  • 好きな画像を読み込んで、マウスでエリア選択するだけで画像保存可能
  • 保存できる画像サイズは四角形(任意サイズと正方形)
  • 正方形サイズはInstagramやAI学習用の画像作成に便利
  • 上記目的で作ったと言っても過言ではないです

実行画面

プログラムを実行すると以下の2つの画像が保存されます。

サイズ:489×445
サイズ:300×300

一枚目の画像は、本記事トップの画像でオレンジ色の部分、二枚目の画像はピンク色の部分です。

オレンジ色の部分はマウスでエリア選択すれば任意サイズで保存できる部分です。

また、ピンク色の部分は指定した正方形サイズで保存できる部分です。

プログラム

以下のコードをみてください。

import glob
import tkinter
import datetime
import tkinter.ttk
import PySimpleGUI as sg
from PIL import Image, ImageTk

# パラメータ
mouse_x = 0
mouse_y = 0
mouse_c = 0
x = 0
y = 0
x1 = 0
y1 = 0
n = 0
x_px = 0
y_px = 0
x1_px = 0
y1_px = 0

# クロップサイズ
px = 300

# マウスボタンを押したときの関数
def mouse_press(e):
    global mouse_c, x, y

    # マウスボタンが押されたときmouse_cに1をセット
    mouse_c = 1

    # 読み込んだ画像に長方形を描画
    # 一段目に関数でアウトラインの線を描画しタグを付ける
    # 二段目の関数で長方形を塗りつぶし一段目とは別のタグを付ける。fillは塗りつぶし、stippleは点描画
    cvs.create_rectangle(e.x, e.y, e.x+1, e.y+1, outline="red", tag="rect1")
    cvs.create_rectangle(e.x, e.y, e.x+1, e.y+1, fill="orange", stipple='gray50', tag="rect2") # 色候補 orange, pink
    cvs.create_rectangle(e.x, e.y, e.x+1, e.y+1, fill="pink", stipple='gray50', tag="rect3")   # 色候補 orange, pink
    
    # マウスボタンを押したときの座標をx, yにセット
    x = e.x
    y = e.y

# マウスを動かしているときの関数
def mouse_move(e):
    global mouse_x, mouse_y, end_x, end_y, x_px, y_px, x1_px, y1_px, px

    # マウス座標がマイナスになったときの判定
    if e.x < 0:
        end_x = 0
    else:
        end_x = min(x, e.x)
    
    if e.y < 0:
        end_y = 0
    else:
        end_y = min(y, e.y)

    # マウスの選択エリアが300px X 300pxサイズ以上になると300px X 300pxのエリアの色が変わるよう判定
    if abs(mouse_x - end_x) and abs(mouse_y - end_y) >= px:
        cvs.coords("rect1", mouse_x, mouse_y, end_x, end_y)
        cvs.coords("rect2", mouse_x, mouse_y, end_x, end_y)
        cvs.coords("rect3", end_x, end_y, end_x+px, end_y+px)

        # 300pxエリアの座標を代入
        x_px = end_x
        y_px = end_y
        x1_px = end_x+px
        y1_px = end_y+px
        
    else:
        cvs.coords("rect1", mouse_x, mouse_y, end_x, end_y)
        cvs.coords("rect2", mouse_x, mouse_y, end_x, end_y)
    
    # マウスが動いた座標を逐次mouse_x, mouse_yにセット
    mouse_x = e.x
    mouse_y = e.y

# マウスボタンを離したときの関数
def mouse_release(e):
    global mouse_c, x1, y1, n

    # マウスボタンを離したときにmouse_cに2をセット
    mouse_c = 2

    # マウスボタンを離したときの座標をx1, y1にセット
    x1 = e.x
    y1 = e.y

    # マウスボタンを離したとき描画した図形を消す
    cvs.delete("rect1")
    cvs.delete("rect2")
    cvs.delete("rect3")

    n += 1

# 選択エリアの画像を保存する関数
def img_save(*args):
    global x, y, x1, y1

    crop_img = read_image.crop(box=args)

    # クロップ画像の保存
    d_today = datetime.date.today()   # 日付の取得
    dt_now = datetime.datetime.now()  # 時間の取得

    crop_img.save('./cnn_act/mov/trimming/' +
                        str(d_today) + str("_") +
                        str(dt_now.hour) + str("_") +
                        str(dt_now.minute) + str("_") +
                        str(dt_now.second) + '.png')


def img_save_2(a, b, c, d, *args):
    global x, y, x1, y1, x_px, y_px, x1_px, y1_px

    crop_img_300 = read_image.crop(box=args)

    if abs(c - a) >= px and abs(d - b) >= px:

        d_today = datetime.date.today()   # 日付の取得
        dt_now = datetime.datetime.now()  # 時間の取得

        crop_img_300.save('./cnn_act/mov/trimming/' + str(px) + str("_") + 
                                str(d_today) + str("_") +
                                str(dt_now.hour) + str("_") +
                                str(dt_now.minute) + str("_") +
                                str(dt_now.second) + '.png')
    
    # 座標の初期化
    x = 0
    y = 0
    x1 = 0
    y1 = 0
    x_px = 0
    y_px = 0
    x1_px = 0
    y1_px = 0


# クロップ画像を保存する判定
def img_main():
    global x, y, x1, y1, mouse_c, n, x_px, y_px, x1_px, y1_px, px

    if x != 0 and x1 != 0:
        if mouse_c == 2 and n == 1:
            img_save(x, y, x1, y1)
            img_save_2(x, y, x1, y1, x_px, y_px, x1_px, y1_px)
            n -= 1

    root.after(100, img_main)

# プログラム実行
if __name__ == "__main__":

    # ファイルを開くポップアップ表示
    fname = sg.popup_get_file('File to open')

    # ファイル読み込み
    f = glob.glob(fname)

    # ファイルを開く
    read_image = Image.open(f[0])

    # 画像サイズを取得
    w, h = read_image.size
    #print('w=', w, 'h=', h)

    root = tkinter.Tk()
    root.title("Tomo Crop Tool")
    root.resizable(False, False)

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

    # Canvasの準備
    cvs = tkinter.Canvas(root, bg="black", width=w, height=h)

    # Canvasに画像を描画
    cvs.create_image(0, 0, image=img, anchor='nw')
    
    # Canvasを配置しマウスのイベントを設定
    cvs.pack()
    cvs.bind("<Motion>", mouse_move)
    cvs.bind("<ButtonPress>", mouse_press)
    cvs.bind("<ButtonRelease>", mouse_release)

    # クロップ画像を保存する判定
    img_main()

    root.mainloop()

ポイント解説

1.プログラムの実行順

コードの下の方に書いてある、if __name__ == " __main__ " : 以降のコードが先に実行されます。

始めに、以下のコードで画像ファイルを開くポップアップを表示させます。

fname = sg.popup_get_file('File to open')

これは、PySimpleGUIでファイルを開くコードです。

fname には画像のパスが入ります。

これを glob.glob( ) のカッコの中に入れると f に代入されます。

f はリストになっているため、リストの一番最初のパス f[0](0番から数えるため)を Image.open( ) のカッコの中に入れます。

すると、Image データとして読み込まれ read_image に代入されます。

次に、この Image データから画像の縦と横のサイズを調べます。

それが以下のコードです。

w, h = read_image.size

このコードで、左辺のwとhに画像の横サイズと縦サイズが代入されます。

なお、画像サイズを調べる理由は、この後 tkinter.Canvas( ) で画像を張り付ける下地(cvs)を準備する際、下地の大きさを画像サイズに合わせるのに必要な情報だからです。

次に、tkinter で画像を表示させるために画像データを変換します。

それが以下のコードです。

img = ImageTk.PhotoImage(image=read_image)

このコードで Image データが tkinter で画像表示できる PhotoImage データに変換されます。

その後、以下のコードで Canvas の準備と画像の張り付けを行います。

# Canvasの準備
cvs = tkinter.Canvas(root, bg="black", width=w, height=h)

# Canvasに画像を描画
cvs.create_image(0, 0, image=img, anchor='nw')

作成したCanvas にマウスのアクションを bind(紐づけ)していきます。

それが以下のコードです。

cvs.bind("<ButtonPress>", mouse_press)
cvs.bind("<Motion>", mouse_move)
cvs.bind("<ButtonRelease>", mouse_release)

mouse_press、mouse_move、mouse_release はコードの前半で定義した関数です。

それぞれの関数には、上から順にマウスの左ボタンを押したとき、ドラックしたままマウスを動かしたとき、マウスの左ボタンを離したときの実行内容が記載されています。

その後、以下の関数でクロップした画像を保存するかどうか判定します。

img_main()

2.mouse_press関数の説明

マウスの左ボタンを押したとき、その座標を起点にして3つの長方形を描画します。

先ほど準備した cvs = tkinter.Canvas( ) に長方形を描くためには、cvs.create_rectangle( ) を使用します。

以下のコードをみてください。

cvs.create_rectangle(e.x, e.y, e.x+1, e.y+1, outline="red", tag="rect1")
cvs.create_rectangle(e.x, e.y, e.x+1, e.y+1, fill="orange", stipple='gray50', tag="rect2") 
cvs.create_rectangle(e.x, e.y, e.x+1, e.y+1, fill="pink", stipple='gray50', tag="rect3")

一段目は、マウスでドラックしたエリアにアウトラインを描くものです。色は赤を指定しています。

二段目は、上記エリアをオレンジ色に塗りつぶすものです。fill は塗りつぶしの意味です。stipple は点描画の意味です。

三段目は、上記エリアをピンク色に塗りつぶすものです。

二段目と三段目は塗りつぶしのコードですが、二段目の色が優先されて表示されます。三段目のピンク色がなぜ必要なのかは後程説明します。

なお、それぞれに tag を付けて識別できるようにしておきます。

また、e.x とe.y でマウスの左ボタンを押したときの座標を x と y にセットします。

3.mouse_move関数の説明

コードの先頭付近で設定したクロップサイズ px = 300 を基準に描画有無の判定をします。以下のコードをみてください。

if abs(mouse_x - end_x) and abs(mouse_y - end_y) >= px:
    cvs.coords("rect1", mouse_x, mouse_y, end_x, end_y)
    cvs.coords("rect2", mouse_x, mouse_y, end_x, end_y)
    cvs.coords("rect3", end_x, end_y, end_x+px, end_y+px)

    # 300pxエリアの座標を代入
    x_px = end_x
    y_px = end_y
    x1_px = end_x+px
    y1_px = end_y+px
        
else:
    cvs.coords("rect1", mouse_x, mouse_y, end_x, end_y)
    cvs.coords("rect2", mouse_x, mouse_y, end_x, end_y)

if 文では、マウスで指定したエリアが 300px X 300px 以上なら tag で指定した rect1, rect2, rect3 の全てを描画するように指定します。

これにより、エリアが 300px X 300px を超えたときに、ピンク色の塗りつぶし部分は固定された状態で描画されます。

もし、マウスで指定したエリアが 300px X 300px 以下なら、else の部分が実行されます。このときは、rect3 のピンク色の塗りつぶしは描画されません。

なお、マウスが動いた座標は以下のコードで逐次 mouse_x と mouse_y にセットされます。

mouse_x = e.x
mouse_y = e.y

4.mouse_release関数の説明

マウスの左ボタンを離したとき、e.x と e.y が x1 と y1 にセットされます。

また、以下のコードで描画した図形を消しておきます。

cvs.delete("rect1")
cvs.delete("rect2")
cvs.delete("rect3")

これをしないと、前に描画した図形が残ってしまいます。

5.img_save_2(a, b, c, d, *args)関数の説明

これは選択エリアの画像を保存する関数です。

なお、img_save(*args)関数はこれに似たものなので説明は省略します。

まず、img_save_2( ) 関数の引数(カッコの中)をみてください。

a ~ d と *args の引数があることに気づいたでしょうか?

それぞれ意味が違います。

まず、a ~ d は一般的な第一から第四引数となります。

使用されている英文字よりも何番目に書かれているかの順番が重要な情報になります。

次に *args はオプションパラメータになります。* はプレフィックスと呼ばれるものです。この * args は追加の位置パラメータに対する引数をタプルにまとめることができます。

つまり、引数が何個あってもその位置情報(パラメータの順番)を保ち、タプルにまとめた形で引数として関数に渡すことができるものです。

なので、以下のようなコードが記載できます。

crop_img_300 = read_image.crop(box=args)

これは以下のコードと同じ意味です。

crop_img_300 = read_image.crop(box=(x_px, y_px, x1_px, y1_px))

box の部分に代入する座標を *args でタプルのまま引数として持ってきているため前者のようなコードを書くことができます。

6.img_main( ) 関数の説明

ここでは、マウスでエリア指定した画像を保存するか判定しています。

最初の if 文では x と x1 がゼロでないとき、つまりマウスで何もしていないときは画像を保存しないという意味です。

また、次の if 文ではマウスの左ボタンを離したとき img_save( ) と img_save_2( ) 関数が実行されるという意味です。

なお、以下のコードで画像保存のタイミングをタイマーでコントロールしています。

このコードがないと画像が保存されません。

root.after(100, img_main)  # 100msec後にimg_main()が実行

まとめ

  • マウスのアクションを紐づけするには bind( ) を使う
  • * args は引数が何個あってもその位置情報(パラメータの順番)を保ち、タプルにまとめた形で引数として関数に渡すことができる
  • root.after( ) で関数を実行するタイミングを設定できる