Python tkinter 画像の傾きを補正するGUIアプリ

まくまく
まくまく
tkinterを使用して画像の傾きを補正するGUIアプリを作成しました。

以下のような用途で使用できると思います。

① 会議に使用するホワイトボードを撮影した写真の傾き補正
② 本や雑誌を撮影した画像の傾き補正
③ 街中で撮った看板等の傾き補正

出力


プログラムを実行すると、このようなウインドウが立ち上がります。左側のボタンの並びがそのまま処理の流れになっています。
①で画像ファイルを選択すると、右側に選択された画像が表示されます。


②では座標を取得します。「座標取得」ボタンを押すと、別ウインドウで画像が立ち上がるので、カードの角の4点をクリックします。このとき、クリックする順番は左上はじまりの反時計回り(左上→左下→右下→右上)です。順番が異なったり、1箇所で2回クリックしてしまうと出力画像が乱れます。

4角のクリックが終わると、このウインドウは不要なのでescで消しておきます。

(ほんとうは、右側の表示エリアの画像をクリックして座標取得したかったのですが、新しいウインドウを立ち上げる方法でしかできませんでした。。)


クリックした座標が左側に表示されます。4点分あるか確認しましょう。


③で射影変換を行います。変換後の幅と高さを指定して「射影変換」ボタンを押すと、右側に変換後の画像が表示されます。

今回の使用したカードは、幅85mm、高さ54mmだったので、サイズを850 x 540としました。街中で撮った看板など実際のアスペクト比がわからない場合は、適当にここの数値を変えていい感じに表示できるまでトライアンドエラーを繰り返すことになります。


④の「変換後ファイル保存」では、プログラムと同じフォルダにjpgファイルが保存されます。

以下がサンプルプログラムとなります。(Pythonは学習中のため無駄な記述などがある場合があることご了承ください)

追記

11/21 変換をやり直すときはプログラムを再度立ち上げ直さないといけません。この辺りは修正が必要ですね。
11/23 cv2.namedWindowを追加して大きい画像にも対応しましたが、、、環境が異なれば挙動が変わるかもしれません。



サンプルプログラム

#ライブラリのインポート
import tkinter as tk
import tkinter.filedialog
import cv2
from PIL import Image, ImageTk
import numpy as np

#フォーマット変換の関数
def format(image_bgr):
    image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)
    image_pil = Image.fromarray(image_rgb)
    image_tk  = ImageTk.PhotoImage(image_pil)
    return image_tk

#リサイズ関数
def resize(img):
    size = 580
    h, w = img.shape[:2]
    cvh = size*h/w
    image_bgr = cv2.resize(img, (size,int(cvh)))
    return image_bgr

#画像選択、表示する関数
def getfile():
    global image_tk
    global img
    global image_bgr
    f_path = tk.filedialog.askopenfilename(title="ファイル選択", initialdir="ディレクトリを入力", \
                                           filetypes=[("Image file", ".png .jpg .jpeg")])
    str_file_path = str(f_path)
    #OpenCVで画像を読み込む
    img = cv2.imread(str_file_path)
    image_bgr = resize(img)
    image_tk = format(image_bgr)
    canvas.create_image(0, 0, image=image_tk, anchor=tk.NW)

#座標取得
def getpoint():
    text_widget.delete("1.0","end")
    points[:0]
    cv2.namedWindow("img", cv2.WINDOW_NORMAL)
    cv2.imshow('img', img)
    cv2.setMouseCallback('img',coordinates)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    exit()

#クリックした座標をリストに格納
def coordinates(event,x,y, flags,param):
    if event == cv2.EVENT_LBUTTONDOWN:
        points.append([x,y])
        text_widget.insert("insert +1lines",str(x)+","+str(y)+"\n")

#射影変換
def per():
    global image_tk
    global img
    global dst
    width = int(en_w.get())
    height = int(en_h.get())
    pts1 = np.float32(points)
    pts2 = np.float32([[0,0], [0,height], [width,height], [width,0]])
    # 変換マトリクスと射影変換
    M = cv2.getPerspectiveTransform(pts1, pts2)
    img = cv2.warpPerspective(img, M, (width, height))
    dst = img
    image_bgr = resize(img)
    image_tk = format(image_bgr)
    canvas.create_image(0, 0, image=image_tk, anchor=tk.NW)

#変換後のファイルを保存
def save():
    cv2.imwrite("ディレクトリを入力/image.jpg", dst)
    
#ウインドウの作成
root = tk.Tk()
#ウインドウのタイトル
root.title("射影変換アプリ")
#ウインドウサイズと位置指定 幅,高さ,x座標,y座標 
root.geometry("800x500+50+50")

#フレームの作成
frame = tk.Frame(root, width=780, height=480, padx=10, pady=10, bg="#D9D9D9")
frame.place(x=10, y=10)
frame_menu = tk.Frame(frame, relief=tk.FLAT, bg="#E6E6E6", bd=2)
frame_menu.place(x=10, y=10, width=150, height=430)
frame_img = tk.Frame(frame, relief=tk.FLAT, bg="#E6E6E6", bd=2)
frame_img.place(x=170, y=10, width=590, height=430)

#座標用 空のリスト
points = []

#選択されたファイル テキストボックスの作成
text_widget = tk.Text(frame_menu, height=6, width=18)
text_widget.grid(row=5, column=0, sticky = tk.W)

#テキストボックスの作成
en_w = tk.Entry(frame_menu, width=15)
en_w.insert(0, int(1000))
en_w.grid(row=9, column=0, sticky = tk.W)
en_h = tk.Entry(frame_menu, width=15)
en_h.insert(0, int(600))
en_h.grid(row=11, column=0, sticky = tk.W)

#ラベルの生成
l_1 = tk.Label(frame_menu,text="① ファイル", relief="flat")
l_1.grid(row=0, column=0, sticky = tk.W)
l_2 = tk.Label(frame_menu,text="② 座標", relief="flat")
l_2.grid(row=2, column=0, sticky = tk.W)
l_3 = tk.Label(frame_menu,text="左上→左下→右下→右上", relief="flat")
l_3.grid(row=4, column=0, sticky = tk.W)
l_4 = tk.Label(frame_menu,text="③ 射影変換", relief="flat")
l_4.grid(row=7, column=0, sticky = tk.W)
l_5 = tk.Label(frame_menu,text="変換後幅", relief="flat")
l_5.grid(row=8, column=0, sticky = tk.W)
l_6 = tk.Label(frame_menu,text="変換後高さ", relief="flat")
l_6.grid(row=10, column=0, sticky = tk.W)
l_7 = tk.Label(frame_menu,text="④ 保存", relief="flat")
l_7.grid(row=13, column=0, sticky = tk.W)

#ボタン作成
button = tk.Button(frame_menu, text="ファイル選択", command=getfile)
button.grid(row=1, column=0, sticky = tk.W)
button_coor = tk.Button(frame_menu, text="座標取得", command=getpoint)
button_coor.grid(row=3, column=0, sticky = tk.W)
button_per = tk.Button(frame_menu, text="射影変換", command=per)
button_per.grid(row=12, column=0, sticky = tk.W)
button_save = tk.Button(frame_menu, text="変換後ファイル保存", command=save)
button_save.grid(row=14, column=0, sticky = tk.W)

#キャンバス作成・配置
canvas = tk.Canvas(frame_img, width=580, height=420)
canvas.grid(row=0, column=0, sticky = tk.W)

#イベントループ
root.mainloop()
「initialdir」にはディレクトリを入力してください。
タイトルとURLをコピーしました