【Python/tkinter】ビットマップデータのビューワーアプリの作り方

ビットマップデータビューワーの作り方解説ページアイキャッチ

このページではビットマップデータのビューワーアプリを tkinter で開発していきたいと思います。

ここでいうビットマップデータとは、Windows Bitmap(.bmp ファイル)ではなく各画素の画素データ(RGB の輝度値)がそのままの形で保存されているデータのことを指します。

ビットマップデータの説明図

画像ファイルを扱う際は、通常は JPEG や PNG などの画像フォーマットを利用すると思います。こういったファイルは Mac のプレビューや Windows のペイントなどで簡単に開くことができるというメリットがあります。

PNG画像をプレビューで開く様子

一方で、ビットマップデータはおそらくこういった PC にプリインストールされているようなアプリでは開くことができません(多分…)。

ビットマップデータがプレビューで開けない様子

PhotoShop などのアプリを使用すれば開くことができますが、そういったアプリをわざわざインストールする必要があります。

ただ、ビットマップデータを表示したいだけであれば、tkinter と PIL を使うと自作で簡単にビューワーアプリを開発することができます。

ということで、今回はこのビットマップデータを表示するビューワーアプリを tkinter と PIL で作成していきたいと思います。

MEMO

このページの猫の画像はリズム727さんによる写真ACからの写真を使用させていただいています

アプリにビットマップデータを表示する方法

まずはビットマップデータを tkinter で作成したアプリに表示する方法について解説していきたいと思います。

ビットマップデータをアプリに表示する手順

ビットマップデータを tkinter で作成したアプリに表示するための手順は下記のようになりますきます。

  1. ビットマップデータファイルを読み込む
  2. 読み込んだファイルを PIL 画像オブジェクトに変換する
  3. PIL 画像オブジェクトを tkinter 画像オブジェクト(PhotoImage クラスのインスタンス)に変換する
  4. 変換後の画像オブジェクトをキャンバスに描画する

まず tkinter で画像を表示するのに便利なのはキャンバスです。キャンバスを作成し、そのキャンバスで create_image メソッドを実行することでアプリ上に画像を表示することができます。

キャンバスに画像を描画してアプリに表示する様子

ただし、create_image メソッドでアプリに画像を表示可能なのは tkinter 画像オブジェクト(PhotoImage クラスのインスタンス)のみです。

一方で、tkinter の PhotoImage クラスはビットマップデータの読み込みに対応していません。

そのため、ビットマップデータファイルを読み込んだのち、それをいったん PIL 画像オブジェクトに変換します。

ビットマップデータをPIL画像に変換する様子

さらに、その PIL 画像オブジェクトを tkinter 画像オブジェクトに変換した後に、その画像オブジェクトをキャンバスに描画してアプリに表示する方法をとります。

tkinter画像に変換後にキャンバスに描画する様子

ここでポイントになるのが「ビットマップデータを PIL 画像オブジェクトに変換する」ところになると思います。

ですが、これも PIL に用意された frombytes 関数を利用すれば簡単に実現することができます。

スポンサーリンク

frombytes 関数

frombytes 関数は、引数で与えられた単なるバイトデータを PIL 画像オブジェクトに変換する関数になります。

PIL 画像オブジェクトに変換されたデータは画像として扱うことが可能で、PIL に用意された関数やメソッドを利用して画像処理などを行うこともできます。

もちろんビットマップデータも frombytes 関数により PIL 画像オブジェクトに変換することが可能ですので、frombytes を利用して読み込んだビットマップデータを画像として扱うことができます。

frombytes関数でビットマップデータをPIL画像に変換する様子

frombytes 関数の定義は下記のようになり、戻り値は PIL 画像オブジェクトとなります。

frombytes
im = frombytes(mode, size, data, decoder_name="raw", *args)

各引数には下記を指定します。

  • 第1引数:モード
  • 第2引数:画像のサイズ
    • (幅, 高さ) などのようにタプルやリスト形式で指定
  • 第3引数:変換するビットマップデータ
  • 第4引数:デコーダーの名前
    • ビットマップデータの場合は 'raw' を指定
  • 第5引数:追加のオプション

特にビットマップデータに対して frombytes 関数を実行する際にポイントになるのは第2引数だと思います。

JPEG や PNG などではヘッダーに画像のサイズ(幅と高さ)が記述されていますので、ヘッダーを解析することでこれらの値を取得することが可能です。

ただし、ビットマップデータの場合は画素のデータがそのまま格納されているだけなので画像のサイズの情報が記述されていません。

そのため、このサイズはユーザーから指定してもらう必要があります。そしてユーザーから指定してもらったサイズ(幅と高さ)を frombytes 関数の第2引数に指定します。

ビットマップデータのビューワーアプリ

ビットマップデータのアプリへの表示(キャンバスへの描画)方法を理解していただいたところで、次はビットマップデータのビューワーアプリのスクリプトを紹介していきたいと思います。

スポンサーリンク

スクリプト

ビットマップデータのビューワーアプリのスクリプトは下記のようになります。

ビットマップデータのビューワー
# -*- coding:utf-8 -*-
import tkinter
import tkinter.filedialog
import tkinter.messagebox
from PIL import Image, ImageTk

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400

class BitmapViewer():
	def __init__(self, master):
		'''コンストラクタ'''

		# 親ウィジェットを設定
		self.master = master

		# 変数を作成
		self.createVariables()

		# ウィジェットを作成
		self.createWidgets()

	def createVariables(self):
		'''アプリで扱う変数を作成する'''

		self.image_width = tkinter.StringVar()
		self.image_height = tkinter.StringVar()
		self.image_mode = tkinter.StringVar()
		self.image_path = tkinter.StringVar()

	def createWidgets(self):
		'''ウィジェットの作成と配置を行う'''

		# キャンバスを配置するフレームを作成
		main_frame = tkinter.Frame(
			self.master,
		)
		main_frame.grid(column=0, row=0)

		# エントリーやボタンを配置するフレームを作成
		sub_frame = tkinter.Frame(
			self.master,
		)
		sub_frame.grid(column=1, row=0)

		# キャンバスの作成と配置
		self.canvas = tkinter.Canvas(
			main_frame,
			bg="white",
			width=CANVAS_WIDTH,
			height=CANVAS_HEIGHT,
		)
		self.canvas.pack()

		# 幅入力用のエントリーの作成と配置
		width_label = tkinter.Label(
			sub_frame,
			text="width",
		)
		width_label.grid(column=0, row=0)

		width_entry = tkinter.Entry(
			sub_frame,
			textvariable=self.image_width,
		)
		width_entry.grid(column=1, row=0)

		# 高さ入力用のエントリーの作成と配置
		height_label = tkinter.Label(
			sub_frame,
			text="height",
		)
		height_label.grid(column=0, row=1)

		height_entry = tkinter.Entry(
			sub_frame,
			textvariable=self.image_height,
		)
		height_entry.grid(column=1, row=1)

		# ファイルパス入力用のエントリーの作成と配置
		file_label = tkinter.Label(
			sub_frame,
			text="file",
		)
		file_label.grid(column=0, row=2)

		file_entry = tkinter.Entry(
			sub_frame,
			textvariable=self.image_path,
		)
		file_entry.grid(column=1, row=2)

		# ファイル選択用ボタンの作成と配置
		select_button = tkinter.Button(
			sub_frame,
			text="ファイル選択",
			command=self.select_file
		)
		select_button.grid(column=0,row=3,columnspan=2)

		# 画像表示用ボタンの作成と配置
		display_button = tkinter.Button(
			sub_frame,
			text="画像表示",
			command=self.display_image
		)
		display_button.grid(column=0,row=4,columnspan=2)

	def select_file(self):
		'''ファイル選択ボタンが押された時の処理'''

		# ファイル選択ダイアログの表示
		path = tkinter.filedialog.askopenfilename()

		if len(path) != 0:
			# ファイルが選択された場合

			# ファイルパスのエントリーを更新
			self.image_path.set(path)


	def display_image(self):
		'''画像表示ボタンが押された時の処理'''

		# エントリーから画像の幅と高さを取得
		width_str = self.image_width.get()
		height_str = self.image_height.get()

		if not width_str or not height_str:
			tkinter.messagebox.showwarning(message="Inpput Width and Height")
			return

		width = int(width_str)
		height = int(height_str)

		# ファイルパスエントリーのパスのデータを開く
		try:
			with open(self.image_path.get(), mode='rb') as f:

				# 1.ビットマップデータファイルを読み込む
				bitmap_data = f.read()
				
				# 2.読み込んだファイルを PIL 画像オブジェクトに変換する
				pil_img = Image.frombytes(
					'RGB',
					[width, height],
					bitmap_data,
					'raw'
				)

		except Exception:
			tkinter.messagebox.showwarning("エラー", "画像が読み込めませんでした")
			return

		# キャンバスのサイズに合わせるための拡大率を計算
		ratio_width = CANVAS_WIDTH / width
		ratio_height = CANVAS_HEIGHT / height
		ratio = min(ratio_width, ratio_height)

		# キャンバスのサイズに合わせてリサイズ
		resize_img = pil_img.resize((int(ratio * width), int(ratio * height)))

		# 3.PIL 画像オブジェクトを tkinter 画像オブジェクトに変換する
		self.img = ImageTk.PhotoImage(resize_img)

		# 4.変換後の画像オブジェクトをキャンバスに描画する
		self.canvas.create_image(
			CANVAS_WIDTH // 2, CANVAS_HEIGHT // 2,
			anchor=tkinter.CENTER,
			image=self.img
		)

app = tkinter.Tk()
app.title("BitmapViewer")
BitmapViewer(app)
app.mainloop()

スポンサーリンク

ビットマップデータの表示

上記のスクリプトを実行すると下の図のようなアプリが起動します。

アプリの起動画面

width 入力欄と height 入力欄と file 入力欄にはそれぞれ下記を入力します。

  • width:ビットマップデータの画像としての幅
  • height:ビットマップデータの画像としての高さ
  • file:ビットマップデータファイルのパス

file 入力欄に入力するパスは「画像選択」ボタンからファイルを選択することで自動入力することも可能です。

MEMO

本スクリプトで対応しているビットマップデータのモードは RGB のみです

グレースケールやアルファチャネル付きのデータには対応していないので注意してください

上記を入力した状態で「画像表示」ボタンを押すと、下の図のようにビットマップデータがキャンバスに描画されて表示されます。

ビットマップデータをアプリに表示した様子

サイズ設定がおかしいと下の図のようにうまく画像が表示されないので注意してください。

サイズ設定がおかしくてうまく画像が表示されない様子

ビットマップデータなんて持ってないよーという方は、このページの最後に紹介するビットマップデータ作成アプリを活用していただければと思います。

スクリプトの解説

スクリプトのポイントになる部分だけを簡単に解説しておきます。

アプリにビットマップデータを表示する方法で解説した通り、ビットマップデータではデータからは画像のサイズが分からないため、ユーザーからサイズを指定してもらえるように幅入力用のエントリーウィジェット(width_entry)と高さ入力用のエントリーウィジェット(height_entry)を用意しています。

サイズ入力用のエントリーの説明図

さらに、読み込むビットマップデータのファイルを指定できるようにビットマップデータファイルのパス入力用のエントリーウィジェット(file_entry)も用意しています。

ファイルパス入力用のエントリーの説明図

また、このパス入力用のエントリーウィジェットには、「ファイル選択」ボタンからファイルを選択された際にそのファイルのパスを自動的に入力するようにしています。

ファイル選択ボタンでファイルパスが自動入力される様子

そして、幅と高さとファイルパスが入力された状態で「画像表示」ボタンが押された際には、ファイルパスに指定されたビットマップデータを読み込んで画像に変換し、キャンバスにその画像を描画することで画像の表示を行なっています。

画像表示ボタンで指定した画像が指定したサイズで表示される様子

この「画像表示」ボタンが押された際に実行されるメソッドが display_image で、ここでアプリにビットマップデータを表示する方法で紹介した下記の処理を実行しています。

  1. ビットマップデータファイルを読み込む
  2. 読み込んだファイルを PIL 画像オブジェクトに変換する
  3. PIL 画像オブジェクトを tkinter 画像オブジェクト(PhotoImage クラスのインスタンス)に変換する
  4. 変換後の画像オブジェクトをキャンバスに描画する

特に 2. の部分で、前述で紹介した frombytes 関数を実行してビットマップデータを PIL 画像オブジェクトへの変換を行なっているところがポイントです。

また、3. と 4. の間では画像をキャンバスにサイズに合わせて拡大縮小しています。

この辺りは等倍で表示し、スクロールバーを利用してスクロールするような形式にカスタマイズするようなこともできますので、興味のある方はご自身でカスタマイズしてみてください。

display_image で実行している処理は、コメントとアプリにビットマップデータを表示する方法を照らし合わせて読んでいただければ分かると思いますので詳しい解説は省略させていただきます。

ビットマップデータ作成アプリ

最後に、ビットマップデータのビューワーアプリに入力するための「ビットマップデータファイル」を持っていない方向けに、ビットマップデータを作成するアプリの紹介をしておきます。

ビットマップデータも Python の PIL を利用すれば簡単に作成することができます。

スポンサーリンク

スクリプト

ビットマップデータ作成アプリのスクリプトは下記のようになります。

ビットマップデータ作成アプリ
# -*- coding:utf-8 -*-
import tkinter
import tkinter.filedialog
import tkinter.messagebox
from PIL import Image, ImageTk

# キャンバスのサイズ設定
CANVAS_WIDTH = 600
CANVAS_HEIGHT = 400

class BitmapCreator():
	def __init__(self, master):
		'''コンストラクタ'''

		# 親ウィジェットを設定
		self.master = master

		# ウィジェット変数を作成
		self.createVariables()

		# ウィジェットを作成
		self.createWidgets()

	def createVariables(self):
		'''アプリで扱う変数を作成する'''

		self.image_path = tkinter.StringVar()

	def createWidgets(self):
		'''ウィジェットの作成と配置を行う'''

		# キャンバスを配置するフレームを作成
		main_frame = tkinter.Frame(
			self.master,
		)
		main_frame.grid(column=0, row=0)

		# エントリーやボタンを配置するフレームを作成
		sub_frame = tkinter.Frame(
			self.master,
		)
		sub_frame.grid(column=1, row=0)

		# キャンバスの作成と配置
		self.canvas = tkinter.Canvas(
			main_frame,
			bg="white",
			width=CANVAS_WIDTH,
			height=CANVAS_HEIGHT,
		)
		self.canvas.pack()

		# 幅表示用のラベルの作成と配置
		width_label = tkinter.Label(
			sub_frame,
			text="width",
		)
		width_label.grid(column=0, row=0)

		self.width_param = tkinter.Label(
			sub_frame,
			text="",
		)
		self.width_param.grid(column=1, row=0)

		# 高さ表示用のラベルの作成と配置
		height_label = tkinter.Label(
			sub_frame,
			text="height",
		)
		height_label.grid(column=0, row=1)

		self.height_param = tkinter.Label(
			sub_frame,
			text="",
		)
		self.height_param.grid(column=1, row=1)

		# ファイルパス表示用のラベルの作成と配置
		file_label = tkinter.Label(
			sub_frame,
			text="file",
		)
		file_label.grid(column=0, row=2)

		self.file_param = tkinter.Label(
			sub_frame,
			text=""
		)
		self.file_param.grid(column=1, row=2)

		# ファイル選択用ボタンの作成と配置
		select_button = tkinter.Button(
			sub_frame,
			text="ファイル選択",
			command=self.select_file
		)
		select_button.grid(column=0,row=3,columnspan=2)

		# ビットマップデータ保存用のボタンの作成と配置
		save_button = tkinter.Button(
			sub_frame,
			text="ビットマップデータを保存",
			command=self.save_bitmap
		)
		save_button.grid(column=0,row=4,columnspan=2)

	def select_file(self):
		'''ファイル選択ボタンが押された時の処理'''

		# ファイル選択ダイアログの表示
		path = tkinter.filedialog.askopenfilename()

		if len(path) != 0:
			# ファイルが選択された場合

			# ファイルパスのエントリーを更新
			self.image_path.set(path)

		# ファイルパスエントリーのパスのデータを開く
		try:
			self.pil_img = Image.open(self.image_path.get())

		except Exception:
			tkinter.messagebox.showwarning("エラー", "画像が読み込めませんでした")
			return

		# キャンバスのサイズに合わせるための拡大率を計算
		ratio_width = CANVAS_WIDTH / self.pil_img.width
		ratio_height = CANVAS_HEIGHT / self.pil_img.height
		ratio = min(ratio_width, ratio_height)

		# キャンバスのサイズに合わせてリサイズ
		resize_img = self.pil_img.resize((int(ratio * self.pil_img.width), int(ratio * self.pil_img.height)))

		# PIL画像をTkinter画像に変換
		self.img = ImageTk.PhotoImage(resize_img)

		# キャンバスに画像を描画
		self.canvas.create_image(
			CANVAS_WIDTH // 2, CANVAS_HEIGHT // 2,
			anchor=tkinter.CENTER,
			image=self.img
		)

		# ラベルを更新
		self.file_param.config(
			text=path
		)

		self.width_param.config(
			text=self.pil_img.width
		)

		self.height_param.config(
			text=self.pil_img.height
		)

	def save_bitmap(self):
		'''ビットマップデータ保存ボタンが押された時の処理'''

		# ファイル選択ダイアログの表示
		path = tkinter.filedialog.asksaveasfilename()

		if len(path):

			# RGBに変換
			rgb_pil_img = self.pil_img.convert('RGB')

			# ビットマップデータへ変換
			bitmap_data = rgb_pil_img.tobytes()

			# ビットマップデータをファイル保存
			with open(path, 'wb') as f:
				f.write(bitmap_data)

app = tkinter.Tk()
app.title("BitmapCreator")
BitmapCreator(app)
app.mainloop()

ビットマップデータの作成

スクリプトを実行すると下の図のような画面のアプリが起動します。

ビットマップデータ作成アプリの起動画面

「ファイル選択」ボタンを押せばファイル選択ダイアログが表示されますので、PNG や JPEG 等の画像ファイルを選択すれば、キャンバスに画像が描画されます。

さらに、width 欄には画像の幅、height 欄には画像の高さ、file 欄には表示中の画像ファイルのパスが表示されます。

アプリに選択した画像が表示される様子

画像が表示されている状態で「ビットマップデータ保存」ボタンを押せば、ファイル選択ダイアログが表示され、ファイル名を入力して Save ボタンを押せば、ビットマップデータが保存されます。

まとめ

このページでは tkinter と PIL を用いたビットマップデータのビューワーアプリの作り方について解説しました。

正直 Python ユーザーの方には使い所はあまりないかもしれません…。Python だと PIL を使えば簡単に PNG や JPEG にエンコードできますので。

ただエンコードするのにちょっと手間のかかるC言語などを利用している場合は、画像処理結果や画像処理途中のデータなどをちょっと確認するのに便利だと思います!是非ビットマップデータの表示にご活用ください!

コメントを残す

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