【Python】簡単な GUI 画像処理アプリ(画像回転アプリ)を作ってみる

簡単な画像処理アプリ開発の解説ページアイキャッチ

このページでは Python で画像の回転を行う画像処理アプリの作り方・サンプルスクリプトの説明をしていきたいと思います。

開発するアプリ

今回開発するアプリについてまず解説します。

アプリの出来上がりは下のアニメーションのようになります。

アプリの完成イメージ

アプリの画面

このページで紹介するアプリの画面は下図のようなものになります。

画像処理アプリのUI

画像表示用にキャンバスを2つ、メッセージ表示用にラベルを1つ、ユーザーから指示を受け付けるためにボタンを2つ用意しています。

アプリの動作

今回開発するアプリでは主に行うことは「画像の読み込み」と「読み込んだ画像の回転」です。

「ファイル選択」ボタンが押された際には、ファイル選択画面を表示し、選択された画像ファイルを読み込んでその画像を左側のキャンバスに表示します。

読み込んだ画像が表示される様子

「画像処理」ボタンが押された際には、読み込んだ画像を回転させ、回転後の画像を右側のキャンバスに表示します。

回転画像が表示される様子

回転は「画像処理」ボタンが押されるたびに10度ずつ回転させるようにしています。

画像処理アプリのクラス設計

アプリは大まかに下記の3つのクラスから構成されています。

  • Model クラス
  • View クラス
  • Controller クラス

各クラスの関係図は下のようになります(MVC モデルを意識していますが若干違うようです)。

各クラスの関係図

今後画像処理を行うアプリをいくつか紹介するページを公開していこうと思っていますが、基本的にここで解説するクラス設計に基づいて開発を行っています。

Model クラス

Model クラスは画像を扱うクラスです。

このクラスでは「画像処理前の画像」と「画像処理後の画像(このアプリでは回転後の画像)」の2つの画像オブジェクトを保持するようにしています。

このアプリの Model では、Controller からの依頼に応じて画像の読み込みと画像の回転を行い、View からの依頼に応じて画像の取得(「画像処理前の画像」と「画像処理後の画像」の取得)を行うようにしています。

画像の読み込み

画像の読み込みは PILPillow) の Image クラスが提供する read メソッドを利用します。

image = Image.read(file_path)
# imageが読み込んだ画像のPIL画像オブジェクトを参照するようになる

画像の回転

画像の読み込みは PILPillow) の Image クラスが提供する rotate メソッドを利用します。

rotate メソッドは画像を回転するメソッドです。

image が参照する PILPillow) 画像オブジェクトを反時計回りに 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_framesub_frame を作成・配置(pack)してアプリ全体のレイアウトを大まかに決め、さらに main_frame の中に canvas_frame と button_frame を作成・配置(grid)することで、一段階詳細なレイアウトを決定しています。

さらにこれらの Frame の中にウィジェットを配置することで、ウィジェットのレイアウトを制御しています。

ウィジェットの作成(LabelCanvas など)については下記で解説していますので、こちらを参考にしていただければと思います。

tkinter解説ページのアイキャッtPython で Tkinter を使ってめちゃくちゃ簡単に GUI アプリを作る

画像の描画

画像の描画は 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"
)

引数 anchortkinter.NW を指定することで、キャンバスの左上を基準とした時の座標(sx, sy)に画像を配置することができるようになります(指定しなければキャンバスの真ん中が基準となる)。

Controller クラス

Controller クラスはユーザーからの入力を受け付け、ModelView の制御を行うクラスです。

ユーザーからの入力(ボタンクリック)を受け付け、その入力に応じて Model に画像読み込みや画像回転を依頼したり、View に文字列や画像の描画を依頼します。

このアプリでは具体的に「ファイル選択」ボタンと「画像処理」ボタンのクリックイベントを受け付けており、それぞれのボタンに応じて Model に下記の処理を依頼しています。

  • ファイル選択ボタン:画像の読み込み
  • 画像処理ボタン:画像の回転

また、after メソッドを利用し、100ms 経過する毎に View に画像等の描画を依頼を行うようにしています。

この辺りのイベントの受け付けや after メソッドについても下記ページで解説しておりますので、詳しく知りたいは方はこちらを参照していただければと思います。

tkinter解説ページのアイキャッtPython で Tkinter を使ってめちゃくちゃ簡単に GUI アプリを作る

スポンサーリンク

画像回転アプリのサンプルスクリプト

ここまで説明してきた画像回転アプリのサンプルスクリプトが下記のようになります。

特に解説は行いませんが、基本的にここまでに説明した内容をプログラミングしただけになりますので、ここまでの解説とコメントを見ながらスクリプトを理解していただければと思います。

画像回転アプリ
# -*- 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 アプリ開発の入門に是非活用してみてください!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です