このページでは Python で画像の回転を行う画像処理アプリの作り方・サンプルスクリプトの説明をしていきたいと思います。
開発するアプリ
今回開発するアプリについてまず解説します。
アプリの出来上がりは下のアニメーションのようになります。
アプリの画面
このページで紹介するアプリの画面は下図のようなものになります。
画像表示用にキャンバスを2つ、メッセージ表示用にラベルを1つ、ユーザーから指示を受け付けるためにボタンを2つ用意しています。
スポンサーリンク
アプリの動作
今回開発するアプリでは主に行うことは「画像の読み込み」と「読み込んだ画像の回転」です。
「ファイル選択」ボタンが押された際には、ファイル選択画面を表示し、選択された画像ファイルを読み込んでその画像を左側のキャンバスに表示します。
「画像処理」ボタンが押された際には、読み込んだ画像を回転させ、回転後の画像を右側のキャンバスに表示します。
回転は「画像処理」ボタンが押されるたびに10度ずつ回転させるようにしています。
画像処理アプリのクラス設計
アプリは大まかに下記の3つのクラスから構成されています。
Model
クラスView
クラスController
クラス
各クラスの関係図は下のようになります(MVC モデルを意識していますが若干違うようです)。
今後画像処理を行うアプリをいくつか紹介するページを公開していこうと思っていますが、基本的にここで解説するクラス設計に基づいて開発を行っています。
Model
クラス
Model
クラスは画像を扱うクラスです。
このクラスでは「画像処理前の画像」と「画像処理後の画像(このアプリでは回転後の画像)」の2つの画像オブジェクトを保持するようにしています。
このアプリの Model
では、Controller
からの依頼に応じて画像の読み込みと画像の回転を行い、View
からの依頼に応じて画像の取得(「画像処理前の画像」と「画像処理後の画像」の取得)を行うようにしています。
画像の読み込み
画像の読み込みは PIL
(Pillow
) の Image
クラスが提供する read
メソッドを利用します。
image = Image.read(file_path)
# imageが読み込んだ画像のPIL画像オブジェクトを参照するようになる
画像の回転
画像の読み込みは PIL
(Pillow
) の Image
クラスが提供する rotate
メソッドを利用します。
rotate
メソッドは画像を回転するメソッドです。
image
が参照する PIL
(Pillow
) 画像オブジェクトを反時計回りに angle
度分回転するためには下記のように rotate
メソッドを実行します。
# imageはPIL画像オブジェクトを参照
rotated_image = image.rotate(angle)
# rotated_imageが回転後のPIL画像オブジェクトを参照するようになる
rotate
メソッドには角度だけでなく、下記のようなオプションも指定することが可能です。
resample
:- 補間処理に適用するアルゴリズムを設定(下記は例)
Image.NEAREST
:最近傍補間-
Image.BILINEAR
:双線形補間
- 補間処理に適用するアルゴリズムを設定(下記は例)
expand
:- 元画像のサイズをはみ出した場合の動作を設定
True
:はみ出した分画像サイズを拡張False
:はみ出しても画像サイズはそのまま(はみ出した分は無くなる)
- 元画像のサイズをはみ出した場合の動作を設定
center
:- 画像回転時の中心座標を指定
- (x, y 座標をタプルで指定)
- 画像回転時の中心座標を指定
translate
:- 画像の平行移動量を指定
- (x, y 座標をタプルで指定)
- 画像の平行移動量を指定
fillcolor
:- 回転を行うことで補ったピクセルの色
- (R, G, B をタプルで指定)
- (R, G, B, A をタプルで指定 ※Aはアルファチャンネル )
- 回転を行うことで補ったピクセルの色
画像の取得
画像の取得は基本的に保持している「画像処理前の画像」or「画像処理後の画像」を返却するだけの処理になります。
ただし View
クラスは Tkinter
に基づいて作成されているので、PIL 画像オブジェクトから Tkinter
用の画像オブジェクトに変換してから返却するようにしています。
画像オブジェクトの変換については下記でまとめていますのでよろしければこちらもご参照ください。
【Python】PIL ⇔ OpenCV2 ⇔ Tkinter 画像オブジェクトの相互変換この画像の取得では、保持している2つの画像のうちのどちらを取得するかをメソッドの引数(Model.BEFORE
or Model.AFTER
)で指定できるようにしています。
スポンサーリンク
View
クラス
View
クラスは UI 画面を扱うクラスです。
GUI 画面上のウィジェットの作成や配置、そのウィジェットに対する文字列の描画や Model
が作成した画像の描画を行います。
ウィジェットの作成と配置
今回作成するアプリでは下図のようにウィジェットを作成し、配置しています。
2つのFrame
オブジェクト main_frame
、sub_frame
を作成・配置(pack
)してアプリ全体のレイアウトを大まかに決め、さらに main_frame
の中に canvas_frame
と button_frame
を作成・配置(grid
)することで、一段階詳細なレイアウトを決定しています。
さらにこれらの Frame
の中にウィジェットを配置することで、ウィジェットのレイアウトを制御しています。
ウィジェットの作成(Label
、Canvas
など)については下記で解説していますので、こちらを参考にしていただければと思います。
画像の描画
画像の描画は Canvas
クラスの提供する create_image
メソッドにより行います。
描画する画像は create_image
メソッド実行前に Model
が保持している画像を取得して描画を行います。
画像の描画はキャンバスの中央に描画するようにしています。
Canvas
クラスのオブジェクトは下記のメソッドにより幅と高さを取得することができます。
winfo_width
:ウィジェットの幅winfo_height
:ウィジェットの高さ
さらに Tkinter の PhotoImage
クラスのオブジェクトは下記のメソッドにより幅と高さを取得することができます。
width
:画像の幅height
:画像の高さ
これらを利用すれば、キャンバスの真ん中に配置した時の画像の左上座標(sx
, sy
)を計算することができます。
この座標を create_image
メソッドに指定すれば、画像を中央に描画することができます。
# canvasは左のキャンバス or 右のキャンバスを参照
# image は tkinter 画像オブジェクトを参照
canvas.create_image(
sx, sy,
image=image,
anchor=tkinter.NW,
tag="image"
)
引数 anchor
に tkinter.NW
を指定することで、キャンバスの左上を基準とした時の座標(sx
, sy
)に画像を配置することができるようになります(指定しなければキャンバスの真ん中が基準となる)。
Controller
クラス
Controller
クラスはユーザーからの入力を受け付け、Model
と View
の制御を行うクラスです。
ユーザーからの入力(ボタンクリック)を受け付け、その入力に応じて Model
に画像読み込みや画像回転を依頼したり、View
に文字列や画像の描画を依頼します。
このアプリでは具体的に「ファイル選択」ボタンと「画像処理」ボタンのクリックイベントを受け付けており、それぞれのボタンに応じて Model
に下記の処理を依頼しています。
- ファイル選択ボタン:画像の読み込み
- 画像処理ボタン:画像の回転
また、after
メソッドを利用し、100ms 経過する毎に View
に画像等の描画を依頼を行うようにしています。
この辺りのイベントの受け付けや after
メソッドについても下記ページで解説しておりますので、詳しく知りたいは方はこちらを参照していただければと思います。
画像回転アプリのサンプルスクリプト
ここまで説明してきた画像回転アプリのサンプルスクリプトが下記のようになります。
特に解説は行いませんが、基本的にここまでに説明した内容をプログラミングしただけになりますので、ここまでの解説とコメントを見ながらスクリプトを理解していただければと思います。
# -*- coding:utf-8 -*-
import tkinter
import tkinter.filedialog
from PIL import Image, ImageTk
class Model():
# 画像処理前か画像処理後かを指定
BEFORE = 1
AFTER = 2
def __init__(self):
# PIL画像オブジェクトを参照
self.before_image = None
self.after_image = None
# Tkinter画像オブジェクトを参照
self.before_image_tk = None
self.after_image_tk = None
def get_image(self, type):
'Tkinter画像オブジェクトを取得する'
if type == Model.BEFORE:
# 処理前画像
if self.before_image is not None:
# Tkinter画像オブジェクトに変換
self.before_image_tk = ImageTk.PhotoImage(self.before_image)
return self.before_image_tk
elif type == Model.AFTER:
# 処理後画像
if self.after_image is not None:
# Tkinter画像オブジェクトに変換
self.after_image_tk = ImageTk.PhotoImage(self.after_image)
return self.after_image_tk
else:
return None
def read(self, path):
'画像の読み込みを行う'
# pathの画像を読み込んでPIL画像オブジェクト生成
self.before_image = Image.open(path)
def rotate(self, angle):
'画像を回転'
# PIL Imageのcropを実行
self.after_image = self.before_image.rotate(
angle,
expand=True
)
class View():
# キャンバス指定用
LEFT_CANVAS = 1
RIGHT_CANVAS = 2
def __init__(self, app, model):
self.master = app
self.model = model
# アプリ内のウィジェットを作成
self.create_widgets()
def create_widgets(self):
'アプリ内にウィジェットを作成・配置する'
# キャンバスのサイズ
canvas_width = 400
canvas_height = 400
# キャンバスとボタンを配置するフレームの作成と配置
self.main_frame = tkinter.Frame(
self.master
)
self.main_frame.pack()
# ラベルを配置するフレームの作成と配置
self.sub_frame = tkinter.Frame(
self.master
)
self.sub_frame.pack()
# キャンバスを配置するフレームの作成と配置
self.canvas_frame = tkinter.Frame(
self.main_frame
)
self.canvas_frame.grid(column=1, row=1)
# ボタンを配置するフレームの作成と配置
self.button_frame = tkinter.Frame(
self.main_frame
)
self.button_frame.grid(column=2, row=1)
# 1つ目のキャンバスの作成と配置
self.left_canvas = tkinter.Canvas(
self.canvas_frame,
width=canvas_width,
height=canvas_height,
bg="#E0E0E0",
)
self.left_canvas.grid(column=1, row=1)
# 2つ目のキャンバスの作成と配置
self.right_canvas = tkinter.Canvas(
self.canvas_frame,
width=canvas_width,
height=canvas_height,
bg="#E0E0E0",
)
self.right_canvas.grid(column=2, row=1)
# ファイル読み込みボタンの作成と配置
self.load_button = tkinter.Button(
self.button_frame,
text="ファイル選択"
)
self.load_button.pack()
# 画像処理ボタンの作成と配置
self.process_button = tkinter.Button(
self.button_frame,
text="画像処理"
)
self.process_button.pack()
# メッセージ表示ラベルの作成と配置
# メッセージ更新用
self.message = tkinter.StringVar()
self.message_label = tkinter.Label(
self.sub_frame,
textvariable=self.message
)
self.message_label.pack()
def draw_image(self, type):
'画像をキャンバスに描画'
# typeに応じて描画先キャンバスを決定
if type == View.LEFT_CANVAS:
canvas = self.left_canvas
image = self.model.get_image(Model.BEFORE)
elif type == View.RIGHT_CANVAS:
canvas = self.right_canvas
image = self.model.get_image(Model.AFTER)
else:
return
if image is not None:
# キャンバス上の画像の左上座標を決定
sx = (canvas.winfo_width() - image.width()) // 2
sy = (canvas.winfo_height() - image.height()) // 2
# キャンバスに描画済みの画像を削除
objs = canvas.find_withtag("image")
for obj in objs:
canvas.delete(obj)
# 画像をキャンバスの真ん中に描画
canvas.create_image(
sx, sy,
image=image,
anchor=tkinter.NW,
tag="image"
)
def draw_message(self, message):
self.message.set(message)
def select_file(self):
'ファイル選択画面を表示'
file_path = tkinter.filedialog.askopenfilename(
initialdir="."
)
return file_path
class Controller():
INTERVAL = 50
def __init__(self, app, model, view):
self.master = app
self.model = model
self.view = view
self.angle = 0
# ラベル表示メッセージ管理用
self.message = "ファイルを読み込んでください"
self.set_events()
def set_events(self):
'受け付けるイベントを設定する'
# 読み込みボタン押し下げイベント受付
self.view.load_button['command'] = self.push_load_button
# 画像処理ボタン押し下げイベント受付
self.view.process_button['command'] = self.push_process_button
# 画像の描画用のタイマーセット
self.master.after(Controller.INTERVAL, self.timer)
def timer(self):
'一定間隔で画像等を描画'
# 画像処理前の画像を左側のキャンバスに描画
self.view.draw_image(
View.LEFT_CANVAS
)
# 画像処理後の画像を右側のキャンバスに描画
self.view.draw_image(
View.RIGHT_CANVAS
)
# ラベルにメッセージを描画
self.view.draw_message(
self.message
)
# 再度タイマー設定
self.master.after(Controller.INTERVAL, self.timer)
def push_load_button(self):
'ファイル選択ボタンが押された時の処理'
# ファイル選択画面表示
file_path = self.view.select_file()
# 画像ファイルの読み込みと描画
if len(file_path) != 0:
self.model.read(file_path)
self.angle = 0
self.model.rotate(0)
# メッセージを更新
self.message = "画像処理ボタンを押してください"
def push_process_button(self):
'画像処理ボタンが押された時の処理'
self.angle = (self.angle + 10) % 360
self.model.rotate(self.angle)
# メッセージを更新
self.message = "画像を回転しました(" + str(self.angle) +"度)"
app = tkinter.Tk()
# アプリのウィンドウのサイズ設定
app.geometry("1000x430")
app.title("画像回転アプリ")
model = Model()
view = View(app, model)
controller = Controller(app, model, view)
app.mainloop()
スポンサーリンク
まとめ
このページでは画像の回転をとり上げて、簡単な画像処理アプリの作り方、回転アプリのサンプルスクリプトの紹介を行いました。
画像の回転を行うためだけに、かなり長いスクリプトを記述する必要がありますが、その分画像処理後の画像がすぐ表示できるというメリットもありますし、何より自分で GUI アプリを開発している感があって楽しく Python や画像処理を学ぶことができると思います。
またアプリの枠組みができると、あとは主に画像処理部分を作り込むことでいろいろな画像処理アプリも作成することが可能です。
Python プログラミングや GUI アプリ開発の入門に是非活用してみてください!